refactor: reorganize project structure and improve code quality
- Move CI/CD configs to config/ci/ directory - Reorganize scripts into categorized directories (deployment, monitoring, testing, utils) - Consolidate documentation into docs/ directory with proper structure - Update linting and testing configurations - Remove obsolete test reports and performance summaries - Add new documentation for code quality tools and contact form security - Improve project organization and maintainability - Fix lint-staged config to only lint JS/TS files - Disable react/react-in-jsx-scope rule for Next.js compatibility - Ignore scripts and test config directories in ESLint
This commit is contained in:
@@ -0,0 +1,36 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'jsdom',
|
||||
roots: ['<rootDir>/src'],
|
||||
testMatch: ['**/__tests__/**/*.test.{ts,tsx}', '**/*.test.{ts,tsx}'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.{ts,tsx}',
|
||||
'!src/**/*.d.ts',
|
||||
'!src/**/*.stories.{ts,tsx}',
|
||||
'!src/**/__tests__/**',
|
||||
],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
branches: 70,
|
||||
functions: 70,
|
||||
lines: 70,
|
||||
statements: 70,
|
||||
},
|
||||
},
|
||||
coverageReporters: ['text', 'lcov', 'html', 'json'],
|
||||
coverageDirectory: 'coverage',
|
||||
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
||||
moduleNameMapper: {
|
||||
'^@/(.*)$': '<rootDir>/src/$1',
|
||||
},
|
||||
transform: {
|
||||
'^.+\\.(ts|tsx)$': 'ts-jest',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'node_modules/(?!(nanoid|next-auth|@auth)/)',
|
||||
],
|
||||
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
|
||||
testTimeout: 10000,
|
||||
verbose: true,
|
||||
maxWorkers: '50%',
|
||||
};
|
||||
@@ -0,0 +1,194 @@
|
||||
require('@testing-library/jest-dom');
|
||||
|
||||
const { TextEncoder, TextDecoder } = require('util');
|
||||
global.TextEncoder = TextEncoder;
|
||||
global.TextDecoder = TextDecoder;
|
||||
|
||||
jest.mock('next-auth', () => {
|
||||
return {
|
||||
__esModule: true,
|
||||
default: jest.fn(() => ({
|
||||
handlers: {
|
||||
authOptions: {
|
||||
providers: [],
|
||||
callbacks: {},
|
||||
pages: {},
|
||||
session: {},
|
||||
},
|
||||
},
|
||||
signIn: jest.fn(),
|
||||
signOut: jest.fn(),
|
||||
auth: jest.fn(),
|
||||
})),
|
||||
getServerSession: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('next-auth/providers/credentials', () =>
|
||||
jest.fn(() => ({
|
||||
name: '邮箱密码',
|
||||
credentials: {
|
||||
email: { label: '邮箱', type: 'email' },
|
||||
password: { label: '密码', type: 'password' },
|
||||
},
|
||||
authorize: jest.fn(),
|
||||
}))
|
||||
);
|
||||
|
||||
jest.mock('nanoid', () => ({
|
||||
nanoid: jest.fn(() => 'test-id-123'),
|
||||
}));
|
||||
|
||||
jest.mock('next/dynamic', () => ({
|
||||
__esModule: true,
|
||||
default: (importFn, options) => {
|
||||
const MockComponent = (props) => null;
|
||||
MockComponent.displayName = 'DynamicComponent';
|
||||
MockComponent.preload = () => Promise.resolve();
|
||||
return MockComponent;
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('next/server', () => ({
|
||||
NextRequest: class MockNextRequest {
|
||||
constructor(input, init = {}) {
|
||||
this.url = typeof input === 'string' ? input : input.url;
|
||||
this.method = init.method || 'GET';
|
||||
this.headers = new Headers(init.headers);
|
||||
this.body = init.body;
|
||||
}
|
||||
|
||||
async json() {
|
||||
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
||||
}
|
||||
},
|
||||
NextResponse: {
|
||||
json: (body, init = {}) => ({
|
||||
status: init.status || 200,
|
||||
json: async () => body,
|
||||
}),
|
||||
},
|
||||
}));
|
||||
|
||||
global.console = {
|
||||
...console,
|
||||
error: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
log: jest.fn(),
|
||||
};
|
||||
|
||||
class MockIntersectionObserver {
|
||||
constructor(callback, options = {}) {
|
||||
this.callback = callback;
|
||||
this.options = options;
|
||||
this.elements = new Set();
|
||||
this.observationEntries = [];
|
||||
}
|
||||
|
||||
observe(element) {
|
||||
this.elements.add(element);
|
||||
const entry = {
|
||||
isIntersecting: true,
|
||||
target: element,
|
||||
boundingClientRect: element.getBoundingClientRect ? element.getBoundingClientRect() : {},
|
||||
intersectionRatio: 1,
|
||||
intersectionRect: {},
|
||||
rootBounds: {},
|
||||
time: Date.now(),
|
||||
};
|
||||
this.observationEntries.push(entry);
|
||||
this.callback(this.observationEntries, this);
|
||||
}
|
||||
|
||||
unobserve(element) {
|
||||
this.elements.delete(element);
|
||||
this.observationEntries = this.observationEntries.filter(
|
||||
entry => entry.target !== element
|
||||
);
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.elements.clear();
|
||||
this.observationEntries = [];
|
||||
}
|
||||
|
||||
takeRecords() {
|
||||
return this.observationEntries;
|
||||
}
|
||||
}
|
||||
|
||||
global.IntersectionObserver = MockIntersectionObserver;
|
||||
global.IntersectionObserverEntry = class IntersectionObserverEntry {
|
||||
constructor() {
|
||||
this.isIntersecting = true;
|
||||
this.target = {};
|
||||
this.boundingClientRect = {};
|
||||
this.intersectionRatio = 1;
|
||||
this.intersectionRect = {};
|
||||
this.rootBounds = {};
|
||||
this.time = Date.now();
|
||||
}
|
||||
};
|
||||
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: jest.fn().mockImplementation(query => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
addEventListener: jest.fn(),
|
||||
removeEventListener: jest.fn(),
|
||||
dispatchEvent: jest.fn(),
|
||||
})),
|
||||
});
|
||||
|
||||
global.Request = class Request {
|
||||
constructor(input, init = {}) {
|
||||
this.url = typeof input === 'string' ? input : input.url;
|
||||
this.method = init.method || 'GET';
|
||||
this.headers = new Headers(init.headers);
|
||||
this.body = init.body;
|
||||
}
|
||||
|
||||
async json() {
|
||||
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
||||
}
|
||||
};
|
||||
|
||||
global.Headers = class Headers {
|
||||
constructor(init = {}) {
|
||||
this.headers = {};
|
||||
if (init) {
|
||||
Object.entries(init).forEach(([key, value]) => {
|
||||
this.headers[key.toLowerCase()] = value;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get(name) {
|
||||
return this.headers[name.toLowerCase()];
|
||||
}
|
||||
|
||||
set(name, value) {
|
||||
this.headers[name.toLowerCase()] = value;
|
||||
}
|
||||
};
|
||||
|
||||
global.Response = class Response {
|
||||
constructor(body, init = {}) {
|
||||
this.body = body;
|
||||
this.status = init.status || 200;
|
||||
this.statusText = init.statusText || 'OK';
|
||||
this.headers = new Headers(init.headers);
|
||||
}
|
||||
|
||||
async json() {
|
||||
return typeof this.body === 'string' ? JSON.parse(this.body) : this.body;
|
||||
}
|
||||
|
||||
async text() {
|
||||
return String(this.body);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
ci:
|
||||
collect:
|
||||
numberOfRuns: 3
|
||||
startServerCommand: npm run start
|
||||
startServerReadyPattern: 'Local:'
|
||||
url:
|
||||
- http://localhost:3000/
|
||||
- http://localhost:3000/about
|
||||
- http://localhost:3000/services
|
||||
- http://localhost:3000/products
|
||||
- http://localhost:3000/cases
|
||||
- http://localhost:3000/news
|
||||
- http://localhost:3000/contact
|
||||
settings:
|
||||
preset: desktop
|
||||
onlyCategories:
|
||||
- performance
|
||||
- accessibility
|
||||
- best-practices
|
||||
- seo
|
||||
|
||||
assert:
|
||||
assertions:
|
||||
categories:performance:
|
||||
- error
|
||||
- minScore: 0.9
|
||||
categories:accessibility:
|
||||
- error
|
||||
- minScore: 0.9
|
||||
categories:best-practices:
|
||||
- error
|
||||
- minScore: 0.9
|
||||
categories:seo:
|
||||
- error
|
||||
- minScore: 0.9
|
||||
first-contentful-paint:
|
||||
- error
|
||||
- maxNumericValue: 2000
|
||||
largest-contentful-paint:
|
||||
- error
|
||||
- maxNumericValue: 3000
|
||||
cumulative-layout-shift:
|
||||
- error
|
||||
- maxNumericValue: 0.1
|
||||
total-blocking-time:
|
||||
- error
|
||||
- maxNumericValue: 300
|
||||
speed-index:
|
||||
- error
|
||||
- maxNumericValue: 3000
|
||||
|
||||
upload:
|
||||
target: temporary-public-storage
|
||||
|
||||
settings:
|
||||
output: html
|
||||
outputPath: lighthouse-reports
|
||||
Reference in New Issue
Block a user