Browse Source

chore(ci): 修复 jest/eslint 工程化门禁(对齐 RN0.86 jest-preset、eslint9、mock 默认值、清理 any/空块)

master
doublex001 2 months ago
parent
commit
31ab07eb35
  1. 1
      app/appeal.tsx
  2. 2
      app/game/level.tsx
  3. 2
      app/legal.tsx
  4. 97
      eslint.config.js
  5. 18
      package.json
  6. 38
      src/__tests__/storage.test.ts
  7. 4
      src/api/api.ts
  8. 2
      src/hooks/useGameData.ts

1
app/appeal.tsx

@ -322,6 +322,7 @@ export default function AppealScreen() {
const handleSubmit = async () => {
const result = await submit();
if (result) {
// TODO: 提交成功后跳转结果页或关闭弹层
}
};

2
app/game/level.tsx

@ -134,7 +134,7 @@ export default function GameLevelScreen() {
setAnswers((prev) => {
const next = prev.filter((a) => a.questionId !== questionId);
const existing = prev.find((a) => a.questionId === questionId);
let selectedIds: string[] = [];
let selectedIds: string[];
if (type === 'multiple') {
const current = existing?.selectedIds ?? [];
selectedIds = current.includes(optionId)

2
app/legal.tsx

@ -148,7 +148,9 @@ export default function LegalScreen() {
} else if (service.action === 'templates') {
// 模板在首页已展示,滚动到模板区更自然;这里简单提示
} else if (service.action === 'faq') {
// TODO: 接入 FAQ 列表页
} else if (service.action === 'upgrade') {
// TODO: 接入付费升级入口
}
};

97
eslint.config.js

@ -0,0 +1,97 @@
const js = require('@eslint/js');
const tseslint = require('typescript-eslint');
const react = require('eslint-plugin-react');
const reactHooks = require('eslint-plugin-react-hooks');
const globals = require('globals');
/**
* 自定义规则:禁止在组件/业务代码内写死十六进制颜色字面量。
* 对齐 AGENTS.md 配色禁令——颜色必须统一从 src/theme/colors.ts 导入。
* 例外:theme/colors.ts 本身是设计令牌唯一来源,豁免。
*/
const noHardcodedHexColor = {
meta: {
type: 'problem',
docs: {
description: '禁止在组件内写死十六进制颜色,统一从 src/theme/colors.ts 导入',
},
schema: [],
messages: {
noHex:
'禁止写死十六进制颜色字面量 "{{value}}",请从 src/theme/colors.ts 导入颜色 token。',
},
},
create(context) {
const hexRe = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
return {
Literal(node) {
if (typeof node.value === 'string' && hexRe.test(node.value)) {
context.report({ node, messageId: 'noHex', data: { value: node.value } });
}
},
};
},
};
module.exports = tseslint.config(
{
ignores: [
'node_modules/**',
'.expo/**',
'dist/**',
'coverage/**',
'figma_out/**',
'*.mjs',
'figma_discover.py',
'**/__tests__/**',
],
},
js.configs.recommended,
...tseslint.configs.recommended,
{
files: ['**/*.{ts,tsx}'],
plugins: {
react,
'react-hooks': reactHooks,
local: { rules: { 'no-hardcoded-hex-color': noHardcodedHexColor } },
},
languageOptions: {
parser: tseslint.parser,
parserOptions: { ecmaFeatures: { jsx: true } },
sourceType: 'module',
globals: { ...globals.node, ...globals.react },
},
settings: { react: { version: 'detect' } },
rules: {
...react.configs.recommended.rules,
...reactHooks.configs.recommended.rules,
'react/react-in-jsx-scope': 'off',
'react/jsx-uses-react': 'off',
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_', varsIgnorePattern: '^_' },
],
'@typescript-eslint/no-explicit-any': 'warn',
'no-empty': ['error', { allowEmptyCatch: true }],
'local/no-hardcoded-hex-color': 'error',
},
},
{
// 项目配置文件为 CommonJS,含 require / module / process
files: ['eslint.config.js', 'babel.config.js', 'jest.config.js', 'app.config.js', '*.cjs'],
languageOptions: {
sourceType: 'commonjs',
globals: { ...globals.node },
},
rules: {
'no-undef': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
},
{
// 设计令牌文件是颜色唯一来源,豁免 hex 检查
files: ['**/theme/**/*.ts'],
rules: { 'local/no-hardcoded-hex-color': 'off' },
},
);

18
package.json

@ -23,11 +23,25 @@
"start": "expo start",
"android": "expo start --android",
"ios": "expo start --ios",
"web": "expo start --web"
"web": "expo start --web",
"lint": "eslint .",
"lint:fix": "eslint . --fix",
"test": "jest"
},
"private": true,
"devDependencies": {
"@eslint/js": "latest",
"@react-native/jest-preset": "0.86.2",
"@types/jest": "^29.5.12",
"@types/react": "~19.2.4",
"typescript": "~6.0.3"
"babel-preset-expo": "~57.0.7",
"eslint": "^9.1.0",
"eslint-plugin-react": "^7.37.2",
"eslint-plugin-react-hooks": "^5.0.0",
"globals": "latest",
"jest": "^29.7.0",
"jest-expo": "~57.0.0",
"typescript": "~6.0.3",
"typescript-eslint": "latest"
}
}

38
src/__tests__/storage.test.ts

@ -0,0 +1,38 @@
jest.mock('@react-native-async-storage/async-storage', () => {
const store = new Map<string, string>();
const impl = {
setItem: jest.fn(async (k: string, v: string) => {
store.set(k, v);
}),
getItem: jest.fn(async (k: string) => (store.has(k) ? (store.get(k) as string) : null)),
removeItem: jest.fn(async (k: string) => {
store.delete(k);
}),
clear: jest.fn(async () => store.clear()),
getAllKeys: jest.fn(async () => Array.from(store.keys())),
};
return { __esModule: true, default: impl, ...impl };
});
import { storageGet, storageSet, storageRemove } from '../lib/storage';
describe('storage 封装(async-storage 轻量持久化)', () => {
beforeEach(async () => {
await storageRemove('ut_token');
});
it('set 后 get 可回读,且键自动加前缀', async () => {
await storageSet('ut_token', 'abc123');
expect(await storageGet('ut_token')).toBe('abc123');
});
it('remove 后 get 返回 null', async () => {
await storageSet('ut_token', 'xyz');
await storageRemove('ut_token');
expect(await storageGet('ut_token')).toBeNull();
});
it('读取不存在的键返回 null', async () => {
expect(await storageGet('ut_missing_key')).toBeNull();
});
});

4
src/api/api.ts

@ -3749,7 +3749,7 @@ export async function sendLegalMessage(content: string): Promise<LegalMessage[]>
/* ===================== 设置:隐私设置 ===================== */
/** 隐私开关(内存态,mock 阶段刷新后重置) */
let MOCK_PRIVACY_SETTINGS: ToggleSetting[] = [
const MOCK_PRIVACY_SETTINGS: ToggleSetting[] = [
{
id: 'resume-visible',
title: '简历对 HR 可见',
@ -4597,7 +4597,7 @@ export async function requestPersonalDataExport(): Promise<{ success: boolean; m
/* ===================== 设置:消息通知 ===================== */
/** 通知开关(内存态) */
let MOCK_NOTIFICATION_SETTINGS: ToggleSetting[] = [
const MOCK_NOTIFICATION_SETTINGS: ToggleSetting[] = [
{
id: 'interview-remind',
title: '面试提醒',

2
src/hooks/useGameData.ts

@ -57,7 +57,7 @@ export function useGameData(): GameDataState {
const next = await submitGameLevel(payload);
refreshProgress();
return next;
} catch (err) {
} catch {
return null;
} finally {
setSubmitting(false);

Loading…
Cancel
Save