feat(miniprogram): Token 常量生成脚本 + useCanvasTokens hook (E2-1 Phase 2)
- 新增 scripts/generate-tokens.ts 从 SCSS 解析 CSS 变量生成 token-values.ts - 新增 useCanvasTokens hook 供 Canvas 组件适老化/医生端切换 - vitest include 扩展覆盖 scripts/__tests__/ - 10 单元测试覆盖 SCSS 解析和变量替换
This commit is contained in:
@@ -4,6 +4,7 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"description": "HMS 健康管理平台患者小程序",
|
"description": "HMS 健康管理平台患者小程序",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"generate-tokens": "tsx scripts/generate-tokens.ts",
|
||||||
"build:weapp": "taro build --type weapp",
|
"build:weapp": "taro build --type weapp",
|
||||||
"dev:weapp": "taro build --type weapp --watch",
|
"dev:weapp": "taro build --type weapp --watch",
|
||||||
"lint": "eslint src/",
|
"lint": "eslint src/",
|
||||||
|
|||||||
97
apps/miniprogram/scripts/__tests__/generate-tokens.test.ts
Normal file
97
apps/miniprogram/scripts/__tests__/generate-tokens.test.ts
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import {
|
||||||
|
parseTokensFromScss,
|
||||||
|
parseScssVariables,
|
||||||
|
resolveTokenValues,
|
||||||
|
} from '../generate-tokens';
|
||||||
|
|
||||||
|
describe('parseScssVariables', () => {
|
||||||
|
it('extracts SCSS variable declarations', () => {
|
||||||
|
const scss = '$pri: #C4623A;\n$bg: #F5F0EB;';
|
||||||
|
const vars = parseScssVariables(scss);
|
||||||
|
expect(vars).toEqual({ pri: '#C4623A', bg: '#F5F0EB' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ignores comments and non-variable lines', () => {
|
||||||
|
const scss = '// comment\n$pri: #C4623A;';
|
||||||
|
const vars = parseScssVariables(scss);
|
||||||
|
expect(vars).toEqual({ pri: '#C4623A' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles values with spaces and units', () => {
|
||||||
|
const scss = '$shadow-sm: 0 1px 4px rgba(45, 42, 38, 0.06);\n$r: 16px;';
|
||||||
|
const vars = parseScssVariables(scss);
|
||||||
|
expect(vars['shadow-sm']).toBe('0 1px 4px rgba(45, 42, 38, 0.06)');
|
||||||
|
expect(vars['r']).toBe('16px');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseTokensFromScss', () => {
|
||||||
|
it('extracts --tk-* CSS variables from page selector', () => {
|
||||||
|
const scss = `page {
|
||||||
|
--tk-font-h1: 28px;
|
||||||
|
--tk-font-body: 16px;
|
||||||
|
}`;
|
||||||
|
const tokens = parseTokensFromScss(scss, 'page');
|
||||||
|
expect(tokens).toEqual({ 'font-h1': '28px', 'font-body': '16px' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('extracts from class selector', () => {
|
||||||
|
const scss = `.elder-mode {
|
||||||
|
--tk-font-h1: 32px;
|
||||||
|
}`;
|
||||||
|
const tokens = parseTokensFromScss(scss, '.elder-mode');
|
||||||
|
expect(tokens).toEqual({ 'font-h1': '32px' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('captures SCSS variable references as-is for later resolution', () => {
|
||||||
|
const scss = `page {
|
||||||
|
--tk-pri: #{$pri};
|
||||||
|
--tk-font-body: 16px;
|
||||||
|
}`;
|
||||||
|
const tokens = parseTokensFromScss(scss, 'page');
|
||||||
|
expect(tokens['pri']).toBe('#{$pri}');
|
||||||
|
expect(tokens['font-body']).toBe('16px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns empty object when selector not found', () => {
|
||||||
|
const scss = `page { --tk-font-h1: 28px; }`;
|
||||||
|
const tokens = parseTokensFromScss(scss, '.nonexistent');
|
||||||
|
expect(tokens).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resolveTokenValues', () => {
|
||||||
|
it('replaces SCSS variable references with actual values', () => {
|
||||||
|
const tokensScss = `page {
|
||||||
|
--tk-pri: #{$pri};
|
||||||
|
}`;
|
||||||
|
const varsScss = '$pri: #C4623A;';
|
||||||
|
const result = resolveTokenValues(tokensScss, varsScss, 'page');
|
||||||
|
expect(result).toEqual({ pri: '#C4623A' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles plain values without SCSS references', () => {
|
||||||
|
const tokensScss = `page {
|
||||||
|
--tk-card-radius: 16px;
|
||||||
|
--tk-font-body: 16px;
|
||||||
|
}`;
|
||||||
|
const varsScss = '$r: 16px;';
|
||||||
|
const result = resolveTokenValues(tokensScss, varsScss, 'page');
|
||||||
|
expect(result['card-radius']).toBe('16px');
|
||||||
|
expect(result['font-body']).toBe('16px');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles multiple variable references', () => {
|
||||||
|
const tokensScss = `page {
|
||||||
|
--tk-pri: #{$pri};
|
||||||
|
--tk-pri-l: #{$pri-l};
|
||||||
|
--tk-font-body: 16px;
|
||||||
|
}`;
|
||||||
|
const varsScss = '$pri: #C4623A;\n$pri-l: #F0DDD4;';
|
||||||
|
const result = resolveTokenValues(tokensScss, varsScss, 'page');
|
||||||
|
expect(result['pri']).toBe('#C4623A');
|
||||||
|
expect(result['pri-l']).toBe('#F0DDD4');
|
||||||
|
expect(result['font-body']).toBe('16px');
|
||||||
|
});
|
||||||
|
});
|
||||||
133
apps/miniprogram/scripts/generate-tokens.ts
Normal file
133
apps/miniprogram/scripts/generate-tokens.ts
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
// scripts/generate-tokens.ts
|
||||||
|
// 从 src/styles/tokens.scss + variables.scss 解析 CSS 变量,
|
||||||
|
// 输出 src/styles/token-values.ts 供 JS 运行时使用
|
||||||
|
import { readFileSync, writeFileSync } from 'fs';
|
||||||
|
import { resolve, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
interface TokenMap {
|
||||||
|
[key: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 SCSS 文件中的 CSS 变量声明 */
|
||||||
|
function parseTokensFromScss(
|
||||||
|
scssContent: string,
|
||||||
|
selector: string = 'page',
|
||||||
|
): TokenMap {
|
||||||
|
const tokens: TokenMap = {};
|
||||||
|
const blockRegex = new RegExp(
|
||||||
|
`${selector.replace('.', '\\.')}\\s*\\{([\\s\\S]+?)\\n\\}`,
|
||||||
|
);
|
||||||
|
const blockMatch = scssContent.match(blockRegex);
|
||||||
|
if (!blockMatch) return tokens;
|
||||||
|
|
||||||
|
const varRegex = /--tk-([\w-]+)\s*:\s*([^;]+);/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = varRegex.exec(blockMatch[1])) !== null) {
|
||||||
|
tokens[match[1]] = match[2].trim();
|
||||||
|
}
|
||||||
|
return tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 SCSS 变量(如 $pri: #C4623A)用于替换 #{...} 引用 */
|
||||||
|
function parseScssVariables(scssContent: string): Record<string, string> {
|
||||||
|
const vars: Record<string, string> = {};
|
||||||
|
const regex = /\$([\w-]+)\s*:\s*([^;]+);/g;
|
||||||
|
let match: RegExpExecArray | null;
|
||||||
|
while ((match = regex.exec(scssContent)) !== null) {
|
||||||
|
vars[match[1]] = match[2].trim();
|
||||||
|
}
|
||||||
|
return vars;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 解析 tokens.scss,替换 SCSS 变量引用 */
|
||||||
|
function resolveTokenValues(
|
||||||
|
tokensContent: string,
|
||||||
|
variablesContent: string,
|
||||||
|
selector: string,
|
||||||
|
): TokenMap {
|
||||||
|
const scssVars = parseScssVariables(variablesContent);
|
||||||
|
const rawTokens = parseTokensFromScss(tokensContent, selector);
|
||||||
|
const resolved: TokenMap = {};
|
||||||
|
for (const [key, value] of Object.entries(rawTokens)) {
|
||||||
|
let resolvedValue = value;
|
||||||
|
resolvedValue = resolvedValue.replace(
|
||||||
|
/#\{\$([\w-]+)\}/g,
|
||||||
|
(_, varName) => scssVars[varName] || '',
|
||||||
|
);
|
||||||
|
resolved[key] = resolvedValue;
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateTokenFile(
|
||||||
|
tokensPath: string,
|
||||||
|
variablesPath: string,
|
||||||
|
outputPath: string,
|
||||||
|
): void {
|
||||||
|
const tokensContent = readFileSync(tokensPath, 'utf-8');
|
||||||
|
const variablesContent = readFileSync(variablesPath, 'utf-8');
|
||||||
|
|
||||||
|
const normalTokens = resolveTokenValues(
|
||||||
|
tokensContent,
|
||||||
|
variablesContent,
|
||||||
|
'page',
|
||||||
|
);
|
||||||
|
const elderTokens = resolveTokenValues(
|
||||||
|
tokensContent,
|
||||||
|
variablesContent,
|
||||||
|
'.elder-mode',
|
||||||
|
);
|
||||||
|
const doctorTokens = resolveTokenValues(
|
||||||
|
tokensContent,
|
||||||
|
variablesContent,
|
||||||
|
'.doctor-mode',
|
||||||
|
);
|
||||||
|
|
||||||
|
const output = `// Auto-generated by scripts/generate-tokens.ts — DO NOT EDIT
|
||||||
|
// Generated at: ${new Date().toISOString()}
|
||||||
|
|
||||||
|
export const TOKEN_VALUES = ${JSON.stringify(normalTokens, null, 2)} as const;
|
||||||
|
|
||||||
|
export const ELDER_TOKEN_OVERRIDES = ${JSON.stringify(elderTokens, null, 2)} as const;
|
||||||
|
|
||||||
|
export const DOCTOR_TOKEN_OVERRIDES = ${JSON.stringify(doctorTokens, null, 2)} as const;
|
||||||
|
|
||||||
|
// Canvas 专用:字号(数字,单位 px)
|
||||||
|
export const CANVAS_FONT_NORMAL = {
|
||||||
|
yLabel: 10,
|
||||||
|
xLabel: 10,
|
||||||
|
tooltip: 12,
|
||||||
|
pointNormal: 3,
|
||||||
|
pointAbnormal: 5,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CANVAS_FONT_ELDER = {
|
||||||
|
yLabel: 14,
|
||||||
|
xLabel: 14,
|
||||||
|
tooltip: 16,
|
||||||
|
pointNormal: 5,
|
||||||
|
pointAbnormal: 8,
|
||||||
|
} as const;
|
||||||
|
`;
|
||||||
|
|
||||||
|
writeFileSync(outputPath, output, 'utf-8');
|
||||||
|
console.log(`[generate-tokens] Generated ${outputPath}`);
|
||||||
|
console.log(` Normal: ${Object.keys(normalTokens).length} tokens`);
|
||||||
|
console.log(` Elder: ${Object.keys(elderTokens).length} overrides`);
|
||||||
|
console.log(` Doctor: ${Object.keys(doctorTokens).length} overrides`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI 入口
|
||||||
|
if (process.argv[1]?.endsWith('generate-tokens.ts')) {
|
||||||
|
const root = resolve(__dirname, '..');
|
||||||
|
generateTokenFile(
|
||||||
|
resolve(root, 'src/styles/tokens.scss'),
|
||||||
|
resolve(root, 'src/styles/variables.scss'),
|
||||||
|
resolve(root, 'src/styles/token-values.ts'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { parseTokensFromScss, parseScssVariables, resolveTokenValues, generateTokenFile };
|
||||||
89
apps/miniprogram/src/hooks/useCanvasTokens.ts
Normal file
89
apps/miniprogram/src/hooks/useCanvasTokens.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
TOKEN_VALUES,
|
||||||
|
ELDER_TOKEN_OVERRIDES,
|
||||||
|
DOCTOR_TOKEN_OVERRIDES,
|
||||||
|
CANVAS_FONT_NORMAL,
|
||||||
|
CANVAS_FONT_ELDER,
|
||||||
|
} from '@/styles/token-values';
|
||||||
|
import { useUIStore } from '@/stores/ui';
|
||||||
|
import { useAuthStore } from '@/stores/auth';
|
||||||
|
|
||||||
|
/** Canvas 绘制用的合并 Token */
|
||||||
|
export interface CanvasTokens {
|
||||||
|
pri: string;
|
||||||
|
tx: string;
|
||||||
|
tx2: string;
|
||||||
|
gridColor: string;
|
||||||
|
fontH1: number;
|
||||||
|
fontBody: number;
|
||||||
|
fontBodySm: number;
|
||||||
|
fontCap: number;
|
||||||
|
yLabelFontSize: number;
|
||||||
|
xLabelFontSize: number;
|
||||||
|
tooltipFontSize: number;
|
||||||
|
pointNormalRadius: number;
|
||||||
|
pointAbnormalRadius: number;
|
||||||
|
referenceBandColor: string;
|
||||||
|
areaGradientStart: string;
|
||||||
|
areaGradientEnd: string;
|
||||||
|
lineColor: string;
|
||||||
|
abnormalColor: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function pxToInt(val: string | undefined, fallback: number): number {
|
||||||
|
if (!val) return fallback;
|
||||||
|
return parseInt(val, 10) || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 为 Canvas 组件提供适老化/医生端的 Token 值 */
|
||||||
|
export function useCanvasTokens(): CanvasTokens {
|
||||||
|
const mode = useUIStore((s) => s.mode);
|
||||||
|
const isDoctor = useAuthStore((s) => s.isDoctor());
|
||||||
|
|
||||||
|
return useMemo(() => {
|
||||||
|
const tokens = TOKEN_VALUES as Record<string, string>;
|
||||||
|
const docTokens = DOCTOR_TOKEN_OVERRIDES as Record<string, string>;
|
||||||
|
const elderTokens = ELDER_TOKEN_OVERRIDES as Record<string, string>;
|
||||||
|
|
||||||
|
let pri = tokens['pri'] || '#C4623A';
|
||||||
|
const tx = '#2D2A26';
|
||||||
|
|
||||||
|
if (isDoctor) {
|
||||||
|
pri = docTokens['pri'] || pri;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isElder = mode === 'elder';
|
||||||
|
const fontSet = isElder ? CANVAS_FONT_ELDER : CANVAS_FONT_NORMAL;
|
||||||
|
|
||||||
|
return {
|
||||||
|
pri,
|
||||||
|
tx,
|
||||||
|
tx2: isElder ? '#5A554F' : (tokens['text-secondary'] || '#78716C'),
|
||||||
|
gridColor: isElder ? '#E0DBD5' : '#F3F4F6',
|
||||||
|
fontH1: pxToInt(tokens['font-h1'], 28),
|
||||||
|
fontBody: pxToInt(
|
||||||
|
isElder ? elderTokens['font-body'] : tokens['font-body'],
|
||||||
|
isElder ? 22 : 16,
|
||||||
|
),
|
||||||
|
fontBodySm: pxToInt(
|
||||||
|
isElder ? elderTokens['font-body-sm'] : tokens['font-body-sm'],
|
||||||
|
isElder ? 19 : 14,
|
||||||
|
),
|
||||||
|
fontCap: pxToInt(
|
||||||
|
isElder ? elderTokens['font-cap'] : tokens['font-cap'],
|
||||||
|
isElder ? 18 : 13,
|
||||||
|
),
|
||||||
|
yLabelFontSize: fontSet.yLabel,
|
||||||
|
xLabelFontSize: fontSet.xLabel,
|
||||||
|
tooltipFontSize: fontSet.tooltip,
|
||||||
|
pointNormalRadius: fontSet.pointNormal,
|
||||||
|
pointAbnormalRadius: fontSet.pointAbnormal,
|
||||||
|
referenceBandColor: isElder ? 'rgba(5,150,105,0.15)' : 'rgba(5,150,105,0.08)',
|
||||||
|
areaGradientStart: isElder ? 'rgba(8,145,178,0.20)' : 'rgba(8,145,178,0.15)',
|
||||||
|
areaGradientEnd: 'rgba(8,145,178,0.01)',
|
||||||
|
lineColor: '#0891B2',
|
||||||
|
abnormalColor: '#DC2626',
|
||||||
|
};
|
||||||
|
}, [mode, isDoctor]);
|
||||||
|
}
|
||||||
109
apps/miniprogram/src/styles/token-values.ts
Normal file
109
apps/miniprogram/src/styles/token-values.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
// Auto-generated by scripts/generate-tokens.ts — DO NOT EDIT
|
||||||
|
// Generated at: 2026-05-22T00:11:18.815Z
|
||||||
|
|
||||||
|
export const TOKEN_VALUES = {
|
||||||
|
"pri": "#C4623A",
|
||||||
|
"pri-l": "#F0DDD4",
|
||||||
|
"pri-d": "#8B3E1F",
|
||||||
|
"shadow-btn": "0 4px 16px rgba(196, 98, 58, 0.3)",
|
||||||
|
"shadow-tab": "0 2px 8px rgba(196, 98, 58, 0.25)",
|
||||||
|
"font-display": "72px",
|
||||||
|
"font-hero": "48px",
|
||||||
|
"font-h1": "28px",
|
||||||
|
"font-h2": "22px",
|
||||||
|
"font-body-lg": "18px",
|
||||||
|
"font-body": "16px",
|
||||||
|
"font-body-sm": "14px",
|
||||||
|
"font-num": "30px",
|
||||||
|
"font-num-lg": "34px",
|
||||||
|
"font-cap": "13px",
|
||||||
|
"font-nav": "18px",
|
||||||
|
"font-micro": "11px",
|
||||||
|
"line-height": "1.5",
|
||||||
|
"touch-min": "48px",
|
||||||
|
"btn-primary-h": "52px",
|
||||||
|
"text-secondary": "#78716C",
|
||||||
|
"card-bg": "#FFFFFF",
|
||||||
|
"card-padding": "20px",
|
||||||
|
"card-padding-sm": "16px",
|
||||||
|
"card-padding-lg": "28px",
|
||||||
|
"card-radius": "16px",
|
||||||
|
"gap-2xs": "4px",
|
||||||
|
"gap-xs": "8px",
|
||||||
|
"gap-sm": "12px",
|
||||||
|
"gap-md": "16px",
|
||||||
|
"section-gap": "20px",
|
||||||
|
"gap-lg": "24px",
|
||||||
|
"gap-xl": "32px",
|
||||||
|
"gap-2xl": "48px",
|
||||||
|
"page-padding": "20px",
|
||||||
|
"input-height": "56px",
|
||||||
|
"tabbar-space": "100px",
|
||||||
|
"touch-feedback-opacity": "0.85",
|
||||||
|
"tag-font-size": "11px",
|
||||||
|
"tag-padding-v": "3px",
|
||||||
|
"tag-padding-h": "8px"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const ELDER_TOKEN_OVERRIDES = {
|
||||||
|
"font-display": "80px",
|
||||||
|
"font-hero": "56px",
|
||||||
|
"font-h1": "32px",
|
||||||
|
"font-h2": "25px",
|
||||||
|
"font-body-lg": "22px",
|
||||||
|
"font-body": "22px",
|
||||||
|
"font-body-sm": "19px",
|
||||||
|
"font-num": "34px",
|
||||||
|
"font-num-lg": "40px",
|
||||||
|
"font-cap": "18px",
|
||||||
|
"font-nav": "22px",
|
||||||
|
"font-micro": "17px",
|
||||||
|
"line-height": "1.7",
|
||||||
|
"touch-min": "56px",
|
||||||
|
"btn-primary-h": "60px",
|
||||||
|
"text-secondary": "#5A554F",
|
||||||
|
"card-padding": "28px",
|
||||||
|
"card-padding-sm": "20px",
|
||||||
|
"card-padding-lg": "36px",
|
||||||
|
"card-radius": "20px",
|
||||||
|
"gap-2xs": "6px",
|
||||||
|
"gap-xs": "12px",
|
||||||
|
"gap-sm": "16px",
|
||||||
|
"gap-md": "20px",
|
||||||
|
"section-gap": "28px",
|
||||||
|
"gap-lg": "32px",
|
||||||
|
"gap-xl": "40px",
|
||||||
|
"gap-2xl": "56px",
|
||||||
|
"page-padding": "28px",
|
||||||
|
"input-height": "64px",
|
||||||
|
"tabbar-space": "120px",
|
||||||
|
"touch-feedback-opacity": "0.8",
|
||||||
|
"tag-font-size": "13px",
|
||||||
|
"tag-padding-v": "5px",
|
||||||
|
"tag-padding-h": "12px"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const DOCTOR_TOKEN_OVERRIDES = {
|
||||||
|
"pri": "#3A6B8C",
|
||||||
|
"pri-l": "#D4E5F0",
|
||||||
|
"pri-d": "#2A4F6A",
|
||||||
|
"shadow-btn": "0 4px 16px rgba(58, 107, 140, 0.3)",
|
||||||
|
"shadow-tab": "0 2px 8px rgba(58, 107, 140, 0.25)"
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
// Canvas 专用:字号(数字,单位 px)
|
||||||
|
export const CANVAS_FONT_NORMAL = {
|
||||||
|
yLabel: 10,
|
||||||
|
xLabel: 10,
|
||||||
|
tooltip: 12,
|
||||||
|
pointNormal: 3,
|
||||||
|
pointAbnormal: 5,
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const CANVAS_FONT_ELDER = {
|
||||||
|
yLabel: 14,
|
||||||
|
xLabel: 14,
|
||||||
|
tooltip: 16,
|
||||||
|
pointNormal: 5,
|
||||||
|
pointAbnormal: 8,
|
||||||
|
} as const;
|
||||||
@@ -4,7 +4,7 @@ import path from 'path';
|
|||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
test: {
|
test: {
|
||||||
environment: 'node',
|
environment: 'node',
|
||||||
include: ['__tests__/**/*.test.ts'],
|
include: ['__tests__/**/*.test.ts', 'scripts/__tests__/**/*.test.ts'],
|
||||||
globals: true,
|
globals: true,
|
||||||
setupFiles: ['__tests__/setup.ts'],
|
setupFiles: ['__tests__/setup.ts'],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user