diff --git a/.umirc.ts b/.umirc.ts index ff2a59e..611a2d2 100644 --- a/.umirc.ts +++ b/.umirc.ts @@ -63,7 +63,9 @@ export default defineConfig({ { name: '库存查询', path: '/inventory/stocks', component: './Inventory/Stocks' }, { name: '库存统计', path: '/inventory/stats', component: './Inventory/Stats' }, { name: '库存盘点', path: '/inventory/checks', component: './Inventory/Checks' }, + { name: '盘点单详情', path: '/inventory/checks/:id', component: './Inventory/Checks/detail', hideInMenu: true }, { name: '库存调整', path: '/inventory/adjusts', component: './Inventory/Adjusts' }, + { name: '调整单详情', path: '/inventory/adjusts/:id', component: './Inventory/Adjusts/detail', hideInMenu: true }, { name: 'Excel导入', path: '/inventory/import', component: './Inventory/Import' }, { name: '库存变动记录', path: '/inventory/logs', component: './Inventory/Logs' }, ], @@ -75,6 +77,7 @@ export default defineConfig({ routes: [ { name: '供应商管理', path: '/purchase/suppliers', component: './Purchase/Suppliers' }, { name: '采购订单', path: '/purchase/orders', component: './Purchase/Orders' }, + { name: '新建采购订单', path: '/purchase/orders/new', component: './Purchase/Orders/new', hideInMenu: true }, { name: '采购订单详情', path: '/purchase/orders/:id', component: './Purchase/Orders/detail', hideInMenu: true }, { name: '入库记录', path: '/purchase/receipts', component: './Purchase/Receipts' }, { name: '采购报表', path: '/purchase/stats', component: './Purchase/Stats' }, diff --git a/__mocks__/@umijs/max.ts b/__mocks__/@umijs/max.ts new file mode 100644 index 0000000..6ffe911 --- /dev/null +++ b/__mocks__/@umijs/max.ts @@ -0,0 +1,52 @@ +import { vi } from 'vitest'; + +// Generic API envelope used by the real backend: `{ code, msg, data }`. +// `data` carries the object-shaped fields pages read off `request(...)` +// directly (`list` for the login tenant search, `nodes`/`edges` for the CRM +// graph), all empty so access never throws. +function makeData() { + return { list: [], total: 0, records: [], nodes: [], edges: [] }; +} + +export const request = vi.fn(() => + Promise.resolve({ code: 0, msg: 'ok', success: true, data: makeData() }), +); + +export const history = { + push: vi.fn(), + replace: vi.fn(), + back: vi.fn(), + go: vi.fn(), + location: { pathname: '/', search: '', hash: '', state: null }, +}; + +export const useModel = vi.fn(() => ({ + initialState: { currentUser: { name: 'tester', menuPaths: [] }, menuPaths: [] }, + setInitialState: vi.fn(), + refresh: vi.fn(), + loading: false, +})); + +export const useParams = vi.fn(() => ({ id: '1' })); +export const useSearchParams = vi.fn(() => [new URLSearchParams(), vi.fn()]); +export const useNavigate = vi.fn(() => vi.fn()); +export const useLocation = vi.fn(() => history.location); +export const useAccess = vi.fn(() => ({})); +export const useIntl = vi.fn(() => ({ formatMessage: ({ defaultMessage }: any) => defaultMessage })); + +export const Link = ({ children }: any) => children; +export const NavLink = ({ children }: any) => children; +export const Access = ({ children }: any) => children; +export const Outlet = () => null; + +export const getLocale = vi.fn(() => 'zh-CN'); +export const setLocale = vi.fn(); + +export default { + request, + history, + useModel, + useParams, + useNavigate, + Link, +}; diff --git a/package.json b/package.json index 652ef3e..4f28a15 100644 --- a/package.json +++ b/package.json @@ -8,20 +8,30 @@ "build": "max build", "postinstall": "max setup", "lint": "max lint", - "format": "prettier --write ." + "format": "prettier --write .", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "@ant-design/icons": "^5.6.1", "@ant-design/pro-components": "^2.8.7", "@umijs/max": "^4.4.8", "antd": "^5.24.2", - "axios": "^1.8.4" + "axios": "^1.8.4", + "react": "18.3.1", + "react-dom": "18.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", "@types/react": "^18.3.18", "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.3.4", + "jsdom": "^25.0.1", "prettier": "^3.5.3", - "typescript": "^5.7.3" + "typescript": "^5.7.3", + "vitest": "^2.1.9" }, "packageManager": "pnpm@10.12.4+sha512.5ea8b0deed94ed68691c9bad4c955492705c5eeb8a87ef86bc62c74a26b037b08ff9570f108b2e4dbd1dd1a9186fea925e527f141c648e85af45631074680184" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afc9f92..bbe2262 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,29 +16,56 @@ importers: version: 2.8.10(antd@5.29.3(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/max': specifier: ^4.4.8 - version: 4.6.29(@babel/core@7.23.6)(@types/node@25.3.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2) + version: 4.6.29(@babel/core@7.29.0)(@types/node@25.3.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2) antd: specifier: ^5.24.2 version: 5.29.3(date-fns@2.30.0)(moment@2.30.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) axios: specifier: ^1.8.4 version: 1.13.5 + react: + specifier: 18.3.1 + version: 18.3.1 + react-dom: + specifier: 18.3.1 + version: 18.3.1(react@18.3.1) devDependencies: + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.1) '@types/react': specifier: ^18.3.18 version: 18.3.28 '@types/react-dom': specifier: ^18.3.5 version: 18.3.7(@types/react@18.3.28) + '@vitejs/plugin-react': + specifier: ^4.3.4 + version: 4.3.4(vite@4.5.2(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0)) + jsdom: + specifier: ^25.0.1 + version: 25.0.1 prettier: specifier: ^3.5.3 version: 3.8.1 typescript: specifier: ^5.7.3 version: 5.9.3 + vitest: + specifier: ^2.1.9 + version: 2.1.9(@types/node@25.3.0)(jsdom@25.0.1)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0) packages: + '@adobe/css-tools@4.5.0': + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@ahooksjs/use-request@2.8.15': resolution: {integrity: sha512-xhVaM4fyIiAMdVFuuU5i3CFUdFa/IblF+fvITVMFaUEO3w/V5tVCAF6WIA3T03n1/RPuzRkA7Ao1PFtSGtGelw==} peerDependencies: @@ -185,6 +212,9 @@ packages: '@antfu/utils@0.7.10': resolution: {integrity: sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==} + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/code-frame@7.22.5': resolution: {integrity: sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==} engines: {node: '>=6.9.0'} @@ -405,6 +435,34 @@ packages: peerDependencies: react: '>=16.12.0' + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@csstools/postcss-color-function@1.1.1': resolution: {integrity: sha512-Bc0f62WmHdtRDjf5f3e2STwRAl89N2CLb+9iAwzrv4L2hncrbDwnQD9PCq0gtAt7pOI2leIV08HIBUd4jxD8cw==} engines: {node: ^12 || ^14 || >=16} @@ -1126,6 +1184,131 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + '@rollup/rollup-android-arm-eabi@4.62.0': + resolution: {integrity: sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.62.0': + resolution: {integrity: sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.62.0': + resolution: {integrity: sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.62.0': + resolution: {integrity: sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.62.0': + resolution: {integrity: sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.62.0': + resolution: {integrity: sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': + resolution: {integrity: sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.62.0': + resolution: {integrity: sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.62.0': + resolution: {integrity: sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.62.0': + resolution: {integrity: sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.62.0': + resolution: {integrity: sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-loong64-musl@4.62.0': + resolution: {integrity: sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.62.0': + resolution: {integrity: sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-ppc64-musl@4.62.0': + resolution: {integrity: sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.62.0': + resolution: {integrity: sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.62.0': + resolution: {integrity: sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.62.0': + resolution: {integrity: sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.62.0': + resolution: {integrity: sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.62.0': + resolution: {integrity: sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openbsd-x64@4.62.0': + resolution: {integrity: sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.62.0': + resolution: {integrity: sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.62.0': + resolution: {integrity: sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.62.0': + resolution: {integrity: sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.62.0': + resolution: {integrity: sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.62.0': + resolution: {integrity: sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==} + cpu: [x64] + os: [win32] + '@sinclair/typebox@0.27.10': resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==} @@ -1245,10 +1428,42 @@ packages: react-native: optional: true + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + engines: {node: '>=18'} + peerDependencies: + '@testing-library/dom': ^10.0.0 + '@types/react': ^18.0.0 || ^19.0.0 + '@types/react-dom': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@testing-library/user-event@14.6.1': + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} + peerDependencies: + '@testing-library/dom': '>=7.21.4' + '@trysound/sax@0.2.0': resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==} engines: {node: '>=10.13.0'} + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} @@ -1270,6 +1485,9 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/graceful-fs@4.1.9': resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==} @@ -1720,6 +1938,41 @@ packages: peerDependencies: vite: ^4.2.0 + '@vitejs/plugin-react@4.3.4': + resolution: {integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==} + engines: {node: ^14.18.0 || >=16.0.0} + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + '@vitest/expect@2.1.9': + resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} + + '@vitest/mocker@2.1.9': + resolution: {integrity: sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@2.1.9': + resolution: {integrity: sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==} + + '@vitest/runner@2.1.9': + resolution: {integrity: sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==} + + '@vitest/snapshot@2.1.9': + resolution: {integrity: sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==} + + '@vitest/spy@2.1.9': + resolution: {integrity: sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==} + + '@vitest/utils@2.1.9': + resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -1798,6 +2051,10 @@ packages: resolution: {integrity: sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==} engines: {node: '>=8.9'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + ajv-formats@2.1.1: resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} peerDependencies: @@ -1847,6 +2104,10 @@ packages: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + ansi-styles@6.2.3: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} @@ -1882,6 +2143,13 @@ packages: resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} engines: {node: '>=10'} + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -1929,6 +2197,10 @@ packages: assert@1.5.1: resolution: {integrity: sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -2114,6 +2386,10 @@ packages: resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} engines: {node: '>= 0.8'} + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -2151,6 +2427,10 @@ packages: caniuse-lite@1.0.30001774: resolution: {integrity: sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==} + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + chalk@2.4.2: resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} engines: {node: '>=4'} @@ -2159,6 +2439,10 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -2383,6 +2667,9 @@ packages: resolution: {integrity: sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==} engines: {node: '>= 6'} + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + cssdb@6.6.3: resolution: {integrity: sha512-7GDvDSmE+20+WcSMhP17Q1EVWUrLlbxxpMDqG731n8P99JhnQZHR9YvtjPvEHfjFUjvQJvdpKCjlKOX+xe4UVA==} @@ -2395,6 +2682,10 @@ packages: resolution: {integrity: sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==} engines: {node: '>=8.0.0'} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} @@ -2409,6 +2700,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + data-view-buffer@1.0.2: resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} engines: {node: '>= 0.4'} @@ -2461,10 +2756,17 @@ packages: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + decode-uri-component@0.2.2: resolution: {integrity: sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==} engines: {node: '>=0.10'} + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + deep-is@0.1.4: resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} @@ -2557,6 +2859,12 @@ packages: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + dom-align@1.12.4: resolution: {integrity: sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==} @@ -2722,6 +3030,10 @@ packages: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -2755,6 +3067,9 @@ packages: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} @@ -2897,6 +3212,9 @@ packages: resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} engines: {node: '>=4.0'} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2931,6 +3249,10 @@ packages: resolution: {integrity: sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==} engines: {node: '>=8'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + express-http-proxy@2.1.2: resolution: {integrity: sha512-FXcAcs7Nf/hF73Mzh0WDWPwaOlsEUL/fCHW3L4wU6DH79dypsaxmbnAildCLniFs7HQuuvoiR6bjNVUvGuTb5g==} engines: {node: '>=6.0.0'} @@ -3274,6 +3596,10 @@ packages: htm@3.1.1: resolution: {integrity: sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-entities@2.6.0: resolution: {integrity: sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==} @@ -3306,9 +3632,17 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-browserify@1.0.0: resolution: {integrity: sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -3534,6 +3868,9 @@ packages: resolution: {integrity: sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==} engines: {node: '>=0.10.0'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-regex@1.2.1: resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} engines: {node: '>= 0.4'} @@ -3683,6 +4020,15 @@ packages: resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true + jsdom@25.0.1: + resolution: {integrity: sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^2.11.2 + peerDependenciesMeta: + canvas: + optional: true + jsesc@2.5.2: resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} engines: {node: '>=4'} @@ -3873,6 +4219,9 @@ packages: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -3886,6 +4235,13 @@ packages: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + make-dir@2.1.0: resolution: {integrity: sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==} engines: {node: '>=6'} @@ -4102,6 +4458,9 @@ packages: nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + nwsapi@2.2.24: + resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -4231,6 +4590,9 @@ packages: resolution: {integrity: sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==} engines: {node: '>= 0.10'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -4281,6 +4643,13 @@ packages: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + pbkdf2@3.1.5: resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==} engines: {node: '>= 0.10'} @@ -4621,6 +4990,10 @@ packages: resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} engines: {node: '>= 6'} + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + process-nextick-args@2.0.1: resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} @@ -5166,6 +5539,9 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} @@ -5392,6 +5768,17 @@ packages: engines: {node: '>=14.18.0', npm: '>=8.0.0'} hasBin: true + rollup@4.62.0: + resolution: {integrity: sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + rrweb-cssom@0.7.1: + resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} + + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-applescript@5.0.0: resolution: {integrity: sha512-XcT5rBksx1QdIhlFOCtgZkB99ZEouFZ1E2Kc2LHqNW13U3/74YGdkQRmThTwxy4QIyookibDKYZOPqX//6BlAg==} engines: {node: '>=12'} @@ -5454,6 +5841,10 @@ packages: resolution: {integrity: sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==} engines: {node: '>=11.0.0'} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + scheduler@0.23.2: resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} @@ -5558,6 +5949,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -5647,6 +6041,9 @@ packages: resolution: {integrity: sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==} deprecated: 'Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility' + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + stackframe@1.3.4: resolution: {integrity: sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==} @@ -5658,6 +6055,9 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -5807,6 +6207,9 @@ packages: resolution: {integrity: sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==} engines: {node: '>=0.10.0'} + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + synckit@0.8.5: resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} engines: {node: ^14.18.0 || >=16.0.0} @@ -5867,13 +6270,38 @@ packages: tiny-warning@1.0.3: resolution: {integrity: sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + tinycolor2@1.6.0: resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@1.2.0: + resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} + engines: {node: '>=14.0.0'} + + tinyspy@3.0.2: + resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} + engines: {node: '>=14.0.0'} + titleize@3.0.0: resolution: {integrity: sha512-KxVu8EYHDPBdUYdKZdKtU2aj2XfEx9AfjXxE/Aj0vT06w2icA09Vus1rh6eSu1y01akYg6BjIK/hxyLJINoMLQ==} engines: {node: '>=12'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + tmpl@1.0.5: resolution: {integrity: sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==} @@ -5899,6 +6327,14 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + trim-newlines@3.0.1: resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==} engines: {node: '>=8'} @@ -6083,6 +6519,11 @@ packages: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} + vite-node@2.1.9: + resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + vite@4.5.2: resolution: {integrity: sha512-tBCZBNSBbHQkaGyhGCDUGqeo2ph8Fstyp6FMSvTtsXeZSPpSMGlviAOav2hxVTqFcx8Hj/twtWKsMJXNY0xI8w==} engines: {node: ^14.18.0 || >=16.0.0} @@ -6111,9 +6552,69 @@ packages: terser: optional: true + vite@5.4.21: + resolution: {integrity: sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + vitest@2.1.9: + resolution: {integrity: sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==} + engines: {node: ^18.0.0 || >=20.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/node': ^18.0.0 || >=20.0.0 + '@vitest/browser': 2.1.9 + '@vitest/ui': 2.1.9 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + walker@1.0.8: resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==} @@ -6134,6 +6635,10 @@ packages: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + webpack-5-chain@8.0.1: resolution: {integrity: sha512-Tu1w80WA2Z+X6e7KzGy+cc0A0z+npVJA/fh55q2azMJ030gqz343Kx+yNAstDCeugsepmtDWY2J2IBRW/O+DEA==} engines: {node: '>=10'} @@ -6152,9 +6657,22 @@ packages: webpack-cli: optional: true + whatwg-encoding@3.1.1: + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-fetch@3.6.20: resolution: {integrity: sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==} + whatwg-mimetype@4.0.0: + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} + + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + which-boxed-primitive@1.1.1: resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} engines: {node: '>= 0.4'} @@ -6180,6 +6698,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -6211,6 +6734,13 @@ packages: utf-8-validate: optional: true + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -6256,6 +6786,8 @@ packages: snapshots: + '@adobe/css-tools@4.5.0': {} + '@ahooksjs/use-request@2.8.15(react@18.3.1)': dependencies: lodash.debounce: 4.0.8 @@ -6727,6 +7259,14 @@ snapshots: '@antfu/utils@0.7.10': {} + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + '@babel/code-frame@7.22.5': dependencies: '@babel/highlight': 7.25.9 @@ -6873,90 +7413,90 @@ snapshots: dependencies: '@babel/types': 7.29.0 - '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.23.6)': + '@babel/plugin-syntax-async-generators@7.8.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.23.6)': + '@babel/plugin-syntax-bigint@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.23.6)': + '@babel/plugin-syntax-class-properties@7.12.13(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.23.6)': + '@babel/plugin-syntax-class-static-block@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.23.6)': + '@babel/plugin-syntax-import-attributes@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.23.6)': + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.23.6)': + '@babel/plugin-syntax-json-strings@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.23.6)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.23.6)': + '@babel/plugin-syntax-logical-assignment-operators@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.23.6)': + '@babel/plugin-syntax-nullish-coalescing-operator@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.23.6)': + '@babel/plugin-syntax-numeric-separator@7.10.4(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.23.6)': + '@babel/plugin-syntax-object-rest-spread@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.23.6)': + '@babel/plugin-syntax-optional-catch-binding@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.23.6)': + '@babel/plugin-syntax-optional-chaining@7.8.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.23.6)': + '@babel/plugin-syntax-private-property-in-object@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.23.6)': + '@babel/plugin-syntax-top-level-await@7.14.5(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.28.6 - '@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.23.6)': + '@babel/plugin-transform-modules-commonjs@7.23.3(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.23.6 - '@babel/helper-module-transforms': 7.28.6(@babel/core@7.23.6) + '@babel/core': 7.29.0 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) '@babel/helper-plugin-utils': 7.28.6 '@babel/helper-simple-access': 7.27.1 transitivePeerDependencies: @@ -7009,6 +7549,26 @@ snapshots: reactcss: 1.2.3(react@18.3.1) tinycolor2: 1.6.0 + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@csstools/postcss-color-function@1.1.1(postcss@8.5.6)': dependencies: '@csstools/postcss-progressive-custom-properties': 1.3.0(postcss@8.5.6) @@ -7640,6 +8200,81 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) + '@rollup/rollup-android-arm-eabi@4.62.0': + optional: true + + '@rollup/rollup-android-arm64@4.62.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.62.0': + optional: true + + '@rollup/rollup-darwin-x64@4.62.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.62.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.62.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.62.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.62.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.62.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.62.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.62.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.62.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.62.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.62.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.62.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.62.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.62.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.62.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.62.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.62.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.62.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.62.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.62.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.62.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.62.0': + optional: true + '@sinclair/typebox@0.27.10': {} '@stagewise/toolbar@0.6.2': {} @@ -7759,8 +8394,44 @@ snapshots: optionalDependencies: react-dom: 18.3.1(react@18.3.1) + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.28.6 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.5.0 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@babel/runtime': 7.28.6 + '@testing-library/dom': 10.4.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.28 + '@types/react-dom': 18.3.7(@types/react@18.3.28) + + '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': + dependencies: + '@testing-library/dom': 10.4.1 + '@trysound/sax@0.2.0': {} + '@types/aria-query@5.0.4': {} + '@types/babel__core@7.20.5': dependencies: '@babel/parser': 7.29.0 @@ -7785,15 +8456,17 @@ snapshots: '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/eslint@9.6.1': dependencies: - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@types/estree@1.0.8': {} + '@types/estree@1.0.9': {} + '@types/graceful-fs@4.1.9': dependencies: '@types/node': 25.3.0 @@ -7804,7 +8477,7 @@ snapshots: '@types/history@5.0.0': dependencies: - history: 4.10.1 + history: 5.3.0 '@types/hoist-non-react-statics@3.3.7(@types/react@18.3.28)': dependencies: @@ -7861,7 +8534,7 @@ snapshots: '@types/history': 4.7.11 '@types/react': 18.3.28 '@types/react-router': 5.1.20 - redux: 3.7.2 + redux: 4.2.1 '@types/react-router@5.1.20': dependencies: @@ -8178,7 +8851,7 @@ snapshots: '@umijs/history@5.3.1': dependencies: - '@babel/runtime': 7.23.6 + '@babel/runtime': 7.28.6 query-string: 6.14.1 '@umijs/lint@4.6.29(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3)': @@ -8270,14 +8943,14 @@ snapshots: - typescript - webpack - '@umijs/max@4.6.29(@babel/core@7.23.6)(@types/node@25.3.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2)': + '@umijs/max@4.6.29(@babel/core@7.29.0)(@types/node@25.3.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2)': dependencies: '@umijs/lint': 4.6.29(eslint@8.35.0)(stylelint@14.8.2)(typescript@5.9.3) - '@umijs/plugins': 4.6.29(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@umijs/plugins': 4.6.29(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) antd: 4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) eslint: 8.35.0 stylelint: 14.8.2 - umi: 4.6.29(@babel/core@7.23.6)(@types/node@25.3.0)(@types/react@18.3.28)(eslint@8.35.0)(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(stylelint@14.8.2)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2) + umi: 4.6.29(@babel/core@7.29.0)(@types/node@25.3.0)(@types/react@18.3.28)(eslint@8.35.0)(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(stylelint@14.8.2)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2) transitivePeerDependencies: - '@babel/core' - '@rspack/core' @@ -8337,7 +9010,7 @@ snapshots: dependencies: tsx: 3.12.2 - '@umijs/plugins@4.6.29(@babel/core@7.23.6)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + '@umijs/plugins@4.6.29(@babel/core@7.29.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(antd@4.24.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(rc-field-form@2.7.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@ahooksjs/use-request': 2.8.15(react@18.3.1) '@ant-design/antd-theme-variable': 1.0.0 @@ -8352,7 +9025,7 @@ snapshots: antd-dayjs-webpack-plugin: 1.0.6(dayjs@1.11.19) axios: 0.27.2 babel-plugin-import: 1.13.8 - babel-plugin-styled-components: 2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) + babel-plugin-styled-components: 2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) dayjs: 1.11.19 dva-core: 2.0.4(redux@4.2.1) dva-immer: 1.0.2(dva@2.5.0-beta.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)) @@ -8489,13 +9162,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@umijs/test@4.6.29(@babel/core@7.23.6)': + '@umijs/test@4.6.29(@babel/core@7.29.0)': dependencies: - '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.23.6) + '@babel/plugin-transform-modules-commonjs': 7.23.3(@babel/core@7.29.0) '@jest/types': 27.5.1 '@umijs/bundler-utils': 4.6.29 '@umijs/utils': 4.6.29 - babel-jest: 29.7.0(@babel/core@7.23.6) + babel-jest: 29.7.0(@babel/core@7.29.0) esbuild: 0.21.4 identity-obj-proxy: 3.0.0 isomorphic-unfetch: 4.0.2 @@ -8592,6 +9265,57 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitejs/plugin-react@4.3.4(vite@4.5.2(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0))': + dependencies: + '@babel/core': 7.29.0 + '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.29.0) + '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.29.0) + '@types/babel__core': 7.20.5 + react-refresh: 0.14.2 + vite: 4.5.2(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0) + transitivePeerDependencies: + - supports-color + + '@vitest/expect@2.1.9': + dependencies: + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + tinyrainbow: 1.2.0 + + '@vitest/mocker@2.1.9(vite@5.4.21(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0))': + dependencies: + '@vitest/spy': 2.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 5.4.21(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0) + + '@vitest/pretty-format@2.1.9': + dependencies: + tinyrainbow: 1.2.0 + + '@vitest/runner@2.1.9': + dependencies: + '@vitest/utils': 2.1.9 + pathe: 1.1.2 + + '@vitest/snapshot@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + magic-string: 0.30.21 + pathe: 1.1.2 + + '@vitest/spy@2.1.9': + dependencies: + tinyspy: 3.0.2 + + '@vitest/utils@2.1.9': + dependencies: + '@vitest/pretty-format': 2.1.9 + loupe: 3.2.1 + tinyrainbow: 1.2.0 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -8696,6 +9420,8 @@ snapshots: loader-utils: 2.0.4 regex-parser: 2.3.1 + agent-base@7.1.4: {} + ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -8739,6 +9465,8 @@ snapshots: dependencies: color-convert: 2.0.1 + ansi-styles@5.2.0: {} + ansi-styles@6.2.3: {} antd-dayjs-webpack-plugin@1.0.6(dayjs@1.11.19): @@ -8866,6 +9594,12 @@ snapshots: dependencies: tslib: 2.8.1 + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -8938,6 +9672,8 @@ snapshots: object.assign: 4.1.7 util: 0.10.4 + assertion-error@2.0.1: {} + astral-regex@2.0.0: {} async-function@1.0.0: {} @@ -8976,13 +9712,13 @@ snapshots: transitivePeerDependencies: - debug - babel-jest@29.7.0(@babel/core@7.23.6): + babel-jest@29.7.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 '@jest/transform': 29.7.0 '@types/babel__core': 7.20.5 babel-plugin-istanbul: 6.1.1 - babel-preset-jest: 29.6.3(@babel/core@7.23.6) + babel-preset-jest: 29.6.3(@babel/core@7.29.0) chalk: 4.1.2 graceful-fs: 4.2.11 slash: 3.0.0 @@ -9026,11 +9762,11 @@ snapshots: zod: 3.25.76 zod-validation-error: 2.1.0(zod@3.25.76) - babel-plugin-styled-components@2.1.4(@babel/core@7.23.6)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): + babel-plugin-styled-components@2.1.4(@babel/core@7.29.0)(styled-components@6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)): dependencies: '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-module-imports': 7.28.6 - '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.23.6) + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) lodash: 4.17.23 picomatch: 2.3.1 styled-components: 6.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -9038,30 +9774,30 @@ snapshots: - '@babel/core' - supports-color - babel-preset-current-node-syntax@1.2.0(@babel/core@7.23.6): + babel-preset-current-node-syntax@1.2.0(@babel/core@7.29.0): dependencies: - '@babel/core': 7.23.6 - '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.23.6) - '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.23.6) - '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.23.6) - '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.23.6) - '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.23.6) - '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.23.6) - '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.23.6) - '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.23.6) - '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.23.6) - '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.23.6) - '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.23.6) - '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.23.6) - '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.23.6) - '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.23.6) - '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.23.6) + '@babel/core': 7.29.0 + '@babel/plugin-syntax-async-generators': 7.8.4(@babel/core@7.29.0) + '@babel/plugin-syntax-bigint': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-class-properties': 7.12.13(@babel/core@7.29.0) + '@babel/plugin-syntax-class-static-block': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-json-strings': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-logical-assignment-operators': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-numeric-separator': 7.10.4(@babel/core@7.29.0) + '@babel/plugin-syntax-object-rest-spread': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-catch-binding': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.0) + '@babel/plugin-syntax-private-property-in-object': 7.14.5(@babel/core@7.29.0) + '@babel/plugin-syntax-top-level-await': 7.14.5(@babel/core@7.29.0) - babel-preset-jest@29.6.3(@babel/core@7.23.6): + babel-preset-jest@29.6.3(@babel/core@7.29.0): dependencies: - '@babel/core': 7.23.6 + '@babel/core': 7.29.0 babel-plugin-jest-hoist: 29.6.3 - babel-preset-current-node-syntax: 1.2.0(@babel/core@7.23.6) + babel-preset-current-node-syntax: 1.2.0(@babel/core@7.29.0) balanced-match@1.0.2: {} @@ -9201,6 +9937,8 @@ snapshots: bytes@3.1.2: {} + cac@6.7.14: {} + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -9239,6 +9977,14 @@ snapshots: caniuse-lite@1.0.30001774: {} + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + chalk@2.4.2: dependencies: ansi-styles: 3.2.1 @@ -9250,6 +9996,8 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 + check-error@2.1.3: {} + chokidar@3.5.3: dependencies: anymatch: 3.1.3 @@ -9512,6 +10260,8 @@ snapshots: css-what@6.2.2: {} + css.escape@1.5.1: {} + cssdb@6.6.3: {} cssesc@3.0.0: {} @@ -9520,6 +10270,11 @@ snapshots: dependencies: css-tree: 1.1.3 + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + csstype@3.2.3: {} current-script-polyfill@1.0.0: {} @@ -9531,6 +10286,11 @@ snapshots: data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + data-view-buffer@1.0.2: dependencies: call-bound: 1.0.4 @@ -9574,8 +10334,12 @@ snapshots: decamelize@1.2.0: {} + decimal.js@10.6.0: {} + decode-uri-component@0.2.2: {} + deep-eql@5.0.2: {} + deep-is@0.1.4: {} deepmerge@1.5.2: {} @@ -9653,6 +10417,10 @@ snapshots: dependencies: esutils: 2.0.3 + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + dom-align@1.12.4: {} dom-converter@0.2.0: @@ -9833,6 +10601,8 @@ snapshots: entities@4.5.0: {} + entities@6.0.1: {} + env-paths@2.2.1: {} errno@0.1.8: @@ -9940,6 +10710,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: @@ -10169,6 +10941,10 @@ snapshots: estraverse@5.3.0: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -10215,6 +10991,8 @@ snapshots: dependencies: clone-regexp: 2.2.0 + expect-type@1.3.0: {} + express-http-proxy@2.1.2: dependencies: debug: 3.2.7 @@ -10632,6 +11410,10 @@ snapshots: htm@3.1.1: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-entities@2.6.0: {} html-minifier-terser@6.1.0: @@ -10680,8 +11462,22 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-browserify@1.0.0: {} + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + human-signals@2.1.0: {} human-signals@4.3.1: {} @@ -10897,6 +11693,8 @@ snapshots: is-plain-object@5.0.0: {} + is-potential-custom-element-name@1.0.1: {} + is-regex@1.2.1: dependencies: call-bound: 1.0.4 @@ -11065,6 +11863,34 @@ snapshots: dependencies: argparse: 2.0.1 + jsdom@25.0.1: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + form-data: 4.0.5 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.24 + parse5: 7.3.0 + rrweb-cssom: 0.7.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.19.0 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + jsesc@2.5.2: {} jsesc@3.1.0: {} @@ -11236,6 +12062,8 @@ snapshots: dependencies: js-tokens: 4.0.0 + loupe@3.2.1: {} + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -11250,6 +12078,12 @@ snapshots: dependencies: yallist: 4.0.0 + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + make-dir@2.1.0: dependencies: pify: 4.0.1 @@ -11496,6 +12330,8 @@ snapshots: dependencies: boolbase: 1.0.0 + nwsapi@2.2.24: {} + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -11650,6 +12486,10 @@ snapshots: parse-node-version@1.0.1: {} + parse5@7.3.0: + dependencies: + entities: 6.0.1 + parseurl@1.3.3: {} pascal-case@3.1.2: @@ -11688,6 +12528,10 @@ snapshots: path-type@4.0.0: {} + pathe@1.1.2: {} + + pathval@2.0.1: {} + pbkdf2@3.1.5: dependencies: create-hash: 1.2.0 @@ -12017,6 +12861,12 @@ snapshots: ansi-styles: 3.2.1 react-is: 16.13.1 + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + process-nextick-args@2.0.1: {} process-okam@0.11.10: {} @@ -12778,6 +13628,8 @@ snapshots: react-is@16.13.1: {} + react-is@17.0.2: {} + react-is@18.3.1: {} react-lifecycles-compat@3.0.4: {} @@ -13028,6 +13880,41 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + rollup@4.62.0: + dependencies: + '@types/estree': 1.0.9 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.62.0 + '@rollup/rollup-android-arm64': 4.62.0 + '@rollup/rollup-darwin-arm64': 4.62.0 + '@rollup/rollup-darwin-x64': 4.62.0 + '@rollup/rollup-freebsd-arm64': 4.62.0 + '@rollup/rollup-freebsd-x64': 4.62.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.62.0 + '@rollup/rollup-linux-arm-musleabihf': 4.62.0 + '@rollup/rollup-linux-arm64-gnu': 4.62.0 + '@rollup/rollup-linux-arm64-musl': 4.62.0 + '@rollup/rollup-linux-loong64-gnu': 4.62.0 + '@rollup/rollup-linux-loong64-musl': 4.62.0 + '@rollup/rollup-linux-ppc64-gnu': 4.62.0 + '@rollup/rollup-linux-ppc64-musl': 4.62.0 + '@rollup/rollup-linux-riscv64-gnu': 4.62.0 + '@rollup/rollup-linux-riscv64-musl': 4.62.0 + '@rollup/rollup-linux-s390x-gnu': 4.62.0 + '@rollup/rollup-linux-x64-gnu': 4.62.0 + '@rollup/rollup-linux-x64-musl': 4.62.0 + '@rollup/rollup-openbsd-x64': 4.62.0 + '@rollup/rollup-openharmony-arm64': 4.62.0 + '@rollup/rollup-win32-arm64-msvc': 4.62.0 + '@rollup/rollup-win32-ia32-msvc': 4.62.0 + '@rollup/rollup-win32-x64-gnu': 4.62.0 + '@rollup/rollup-win32-x64-msvc': 4.62.0 + fsevents: 2.3.3 + + rrweb-cssom@0.7.1: {} + + rrweb-cssom@0.8.0: {} + run-applescript@5.0.0: dependencies: execa: 5.1.1 @@ -13079,6 +13966,10 @@ snapshots: sax@1.4.4: optional: true + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 @@ -13233,6 +14124,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -13322,12 +14215,16 @@ snapshots: stable@0.1.8: {} + stackback@0.0.2: {} + stackframe@1.3.4: {} statuses@1.5.0: {} statuses@2.0.2: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -13551,6 +14448,8 @@ snapshots: symbol-observable@1.2.0: {} + symbol-tree@3.2.4: {} + synckit@0.8.5: dependencies: '@pkgr/utils': 2.4.2 @@ -13606,10 +14505,26 @@ snapshots: tiny-warning@1.0.3: {} + tinybench@2.9.0: {} + tinycolor2@1.6.0: {} + tinyexec@0.3.2: {} + + tinypool@1.1.1: {} + + tinyrainbow@1.2.0: {} + + tinyspy@3.0.2: {} + titleize@3.0.0: {} + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + tmpl@1.0.5: {} to-arraybuffer@1.0.1: {} @@ -13630,6 +14545,14 @@ snapshots: toidentifier@1.0.1: {} + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + trim-newlines@3.0.1: {} trim-right@1.0.1: {} @@ -13707,7 +14630,7 @@ snapshots: typescript@5.9.3: {} - umi@4.6.29(@babel/core@7.23.6)(@types/node@25.3.0)(@types/react@18.3.28)(eslint@8.35.0)(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(stylelint@14.8.2)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2): + umi@4.6.29(@babel/core@7.29.0)(@types/node@25.3.0)(@types/react@18.3.28)(eslint@8.35.0)(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(prettier@3.8.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(stylelint@14.8.2)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2): dependencies: '@babel/runtime': 7.23.6 '@umijs/bundler-utils': 4.6.29 @@ -13717,7 +14640,7 @@ snapshots: '@umijs/preset-umi': 4.6.29(@types/node@25.3.0)(@types/react@18.3.28)(less-loader@12.3.1(less@4.1.3)(webpack@5.105.2))(less@4.1.3)(lightningcss@1.22.1)(resolve-url-loader@5.0.0)(rollup@3.30.0)(sass-loader@16.0.7(sass@1.54.0)(webpack@5.105.2))(sass@1.54.0)(terser@5.46.0)(type-fest@0.20.2)(typescript@5.9.3)(webpack@5.105.2) '@umijs/renderer-react': 4.6.29(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@umijs/server': 4.6.29 - '@umijs/test': 4.6.29(@babel/core@7.23.6) + '@umijs/test': 4.6.29(@babel/core@7.29.0) '@umijs/utils': 4.6.29 prettier-plugin-organize-imports: 3.2.4(prettier@3.8.1)(typescript@5.9.3) prettier-plugin-packagejson: 2.4.3(prettier@3.8.1) @@ -13855,6 +14778,24 @@ snapshots: vary@1.1.2: {} + vite-node@2.1.9(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 1.1.2 + vite: 5.4.21(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0) + transitivePeerDependencies: + - '@types/node' + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vite@4.5.2(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0): dependencies: esbuild: 0.18.20 @@ -13868,8 +14809,61 @@ snapshots: sass: 1.54.0 terser: 5.46.0 + vite@5.4.21(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0): + dependencies: + esbuild: 0.21.4 + postcss: 8.5.6 + rollup: 4.62.0 + optionalDependencies: + '@types/node': 25.3.0 + fsevents: 2.3.3 + less: 4.1.3 + lightningcss: 1.22.1 + sass: 1.54.0 + terser: 5.46.0 + + vitest@2.1.9(@types/node@25.3.0)(jsdom@25.0.1)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0): + dependencies: + '@vitest/expect': 2.1.9 + '@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0)) + '@vitest/pretty-format': 2.1.9 + '@vitest/runner': 2.1.9 + '@vitest/snapshot': 2.1.9 + '@vitest/spy': 2.1.9 + '@vitest/utils': 2.1.9 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 1.1.2 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinypool: 1.1.1 + tinyrainbow: 1.2.0 + vite: 5.4.21(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0) + vite-node: 2.1.9(@types/node@25.3.0)(less@4.1.3)(lightningcss@1.22.1)(sass@1.54.0)(terser@5.46.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.3.0 + jsdom: 25.0.1 + transitivePeerDependencies: + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + vm-browserify@1.1.2: {} + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + walker@1.0.8: dependencies: makeerror: 1.0.12 @@ -13893,6 +14887,8 @@ snapshots: web-streams-polyfill@3.3.3: {} + webidl-conversions@7.0.0: {} + webpack-5-chain@8.0.1: dependencies: deepmerge: 1.5.2 @@ -13903,7 +14899,7 @@ snapshots: webpack@5.105.2: dependencies: '@types/eslint-scope': 3.7.7 - '@types/estree': 1.0.8 + '@types/estree': 1.0.9 '@types/json-schema': 7.0.15 '@webassemblyjs/ast': 1.14.1 '@webassemblyjs/wasm-edit': 1.14.1 @@ -13932,8 +14928,19 @@ snapshots: - esbuild - uglify-js + whatwg-encoding@3.1.1: + dependencies: + iconv-lite: 0.6.3 + whatwg-fetch@3.6.20: {} + whatwg-mimetype@4.0.0: {} + + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + which-boxed-primitive@1.1.1: dependencies: is-bigint: 1.1.0 @@ -13983,6 +14990,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -14006,6 +15018,10 @@ snapshots: ws@8.19.0: {} + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/src/pages/CRM/Graph/index.test.tsx b/src/pages/CRM/Graph/index.test.tsx new file mode 100644 index 0000000..dbbdbef --- /dev/null +++ b/src/pages/CRM/Graph/index.test.tsx @@ -0,0 +1,27 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('CRM / Graph page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('shows empty state when graph has no node data', async () => { + render(); + + await waitFor( + () => { + expect(screen.getByText(/No relationship data found/)).toBeInTheDocument(); + }, + { timeout: 5000 }, + ); + }); +}); diff --git a/src/pages/CRM/Relations/index.test.tsx b/src/pages/CRM/Relations/index.test.tsx new file mode 100644 index 0000000..73fea24 --- /dev/null +++ b/src/pages/CRM/Relations/index.test.tsx @@ -0,0 +1,58 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; +import { createRelation } from '@/services/api'; + +describe('CRM / Relations page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders the create relation form', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByPlaceholderText('对方租户ID')).toBeInTheDocument(); + // antd Form buttons are not exposed in the a11y tree in jsdom; query DOM directly + expect(document.querySelector('button[type="submit"]')).toBeInTheDocument(); + }); + + it('renders supplier and customer tabs', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText(/我的供应商/)).toBeInTheDocument(); + expect(screen.getByText(/我的客户/)).toBeInTheDocument(); + }); + + it('calls createRelation when form is submitted with tenant ID', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const tenantInput = screen.getByPlaceholderText('对方租户ID'); + await user.type(tenantInput, 'supplier_001'); + + const createBtn = document.querySelector('button[type="submit"]') as HTMLElement; + await user.click(createBtn); + + await waitFor(() => { + expect(vi.mocked(createRelation)).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pages/Dashboard/index.test.tsx b/src/pages/Dashboard/index.test.tsx new file mode 100644 index 0000000..a58a198 --- /dev/null +++ b/src/pages/Dashboard/index.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../test/smoke'; +import Page from './index'; + +describe('Dashboard page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('displays stat card titles after data loads', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('产品总数')).toBeInTheDocument(); + }); + expect(screen.getByText('总匹数')).toBeInTheDocument(); + expect(screen.getByText('总盘数')).toBeInTheDocument(); + }); + + it('renders quick navigation section', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('快捷入口')).toBeInTheDocument(); + }); + expect(screen.getByText('产品库存')).toBeInTheDocument(); + expect(screen.getByText('库存查询')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Inventory/Adjusts/detail.tsx b/src/pages/Inventory/Adjusts/detail.tsx new file mode 100644 index 0000000..9d3a6ed --- /dev/null +++ b/src/pages/Inventory/Adjusts/detail.tsx @@ -0,0 +1,98 @@ +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { ProTable } from '@ant-design/pro-components'; +import { Button, Card, Descriptions, Modal, Space, Tag, Typography, message } from 'antd'; +import { useEffect, useState } from 'react'; +import { history, useParams } from '@umijs/max'; +import { approveStockAdjust, getStockAdjust } from '@/services/api'; +import type { StockAdjust } from '@/types/api'; + +const STATUS_MAP: Record = { + 0: { text: '待审核', color: 'warning' }, + 1: { text: '已审核', color: 'success' }, + 2: { text: '已驳回', color: 'error' }, +}; + +const AdjustDetailPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [adjust, setAdjust] = useState(null); + + const load = async () => { + if (!id) return; + const res = await getStockAdjust(id); + if (res?.code === 0) setAdjust(res.data as StockAdjust); + }; + + useEffect(() => { load(); }, [id]); + + const handleApprove = (action: number) => { + Modal.confirm({ + title: action === 1 ? '确认审核通过?此操作将实际调整库存。' : '确认驳回此调整单?', + okType: action === 2 ? 'danger' : 'primary', + onOk: async () => { + await approveStockAdjust(id!, action); + message.success(action === 1 ? '已审核通过' : '已驳回'); + load(); + }, + }); + }; + + if (!adjust) return null; + + const status = STATUS_MAP[adjust.status] ?? STATUS_MAP[0]; + + return ( + + + } onClick={() => history.back()}>返回 + 调整单详情 — {adjust.adjustNo} + + + + + {adjust.adjustNo} + {adjust.adjustDate} + {adjust.adjustReason || '-'} + {adjust.operator || '-'} + {adjust.approver || '-'} + + {status.text} + + {adjust.remark && {adjust.remark}} + + {adjust.status === 0 && ( + + handleApprove(1)}>审核通过 + handleApprove(2)}>驳回 + + )} + + + + { + const n = parseFloat(v); + const color = n > 0 ? '#52c41a' : n < 0 ? '#ff4d4f' : undefined; + return {n > 0 ? `+${v}` : v}; + }, + }, + { title: '调整后数量(米)', dataIndex: 'afterQuantity' }, + { title: '备注', dataIndex: 'remark', render: (v: string) => v || '-' }, + ]} + /> + + + ); +}; + +export default AdjustDetailPage; diff --git a/src/pages/Inventory/Adjusts/index.test.tsx b/src/pages/Inventory/Adjusts/index.test.tsx new file mode 100644 index 0000000..1df7c08 --- /dev/null +++ b/src/pages/Inventory/Adjusts/index.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Inventory / Adjusts page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 创建调整单 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /创建调整单/ })).toBeInTheDocument(); + }); + + it('renders 库存调整 table header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('库存调整')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Inventory/Adjusts/index.tsx b/src/pages/Inventory/Adjusts/index.tsx index ce1879e..2200105 100644 --- a/src/pages/Inventory/Adjusts/index.tsx +++ b/src/pages/Inventory/Adjusts/index.tsx @@ -1,8 +1,10 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { ProColumns, ProTable } from '@ant-design/pro-components'; -import { Button, Tag } from 'antd'; -import { listStockAdjusts } from '@/services/api'; -import type { StockAdjust } from '@/types/api'; +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; +import { Button, Col, Form, Input, Modal, Row, Select, Table, Tag, message } from 'antd'; +import { useEffect, useRef, useState } from 'react'; +import { Link } from '@umijs/max'; +import { createStockAdjust, listProducts, listStockAdjusts } from '@/services/api'; +import type { Product, StockAdjust } from '@/types/api'; const STATUS_MAP: Record = { 0: { text: '待审核', color: 'warning' }, @@ -10,38 +12,180 @@ const STATUS_MAP: Record = { 2: { text: '已驳回', color: 'error' }, }; -const columns: ProColumns[] = [ - { title: '调整单号', dataIndex: 'adjustNo' }, - { title: '调整日期', dataIndex: 'adjustDate', valueType: 'date', search: false }, - { title: '调整原因', dataIndex: 'adjustReason', search: false }, - { title: '操作人', dataIndex: 'operator', search: false }, - { - title: '状态', - dataIndex: 'status', - search: false, - render: (_, record) => { - const s = STATUS_MAP[record.status] ?? STATUS_MAP[0]; - return {s.text}; - }, - }, - { title: '创建时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false }, - { title: '操作', valueType: 'option', render: () => [查看] }, -]; +interface DetailRow { key: number; productId?: string; adjustQuantity?: string } +let rowKey = 0; -const AdjustsPage: React.FC = () => ( - - headerTitle="库存调整" - rowKey="adjustId" - columns={columns} - search={false} - request={async (params) => { - const res = await listStockAdjusts({ page: params.current, pageSize: params.pageSize }); - return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 }; - }} - toolBarRender={() => [ - }>创建调整单, - ]} - /> -); +const AdjustsPage: React.FC = () => { + const columns: ProColumns[] = [ + { title: '调整单号', dataIndex: 'adjustNo' }, + { title: '调整日期', dataIndex: 'adjustDate', valueType: 'date', search: false }, + { title: '调整原因', dataIndex: 'adjustReason', search: false }, + { title: '操作人', dataIndex: 'operator', search: false }, + { + title: '状态', + dataIndex: 'status', + search: false, + render: (_, record) => { + const s = STATUS_MAP[record.status] ?? STATUS_MAP[0]; + return {s.text}; + }, + }, + { title: '创建时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false }, + { title: '操作', valueType: 'option', render: (_, record) => [查看] }, + ]; + const actionRef = useRef(); + const [form] = Form.useForm(); + const [open, setOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [products, setProducts] = useState([]); + const [details, setDetails] = useState([{ key: rowKey++ }]); + + useEffect(() => { + listProducts({ page: 1, pageSize: 500 }).then((res) => { + if (res?.code === 0) setProducts(res.data?.list || []); + }); + }, []); + + const handleOpen = () => { + setDetails([{ key: rowKey++ }]); + setOpen(true); + }; + + const handleSubmit = async () => { + let values: { adjustDate: string; adjustReason?: string; remark?: string }; + try { + values = await form.validateFields(); + } catch { + return; + } + const validDetails = details.filter((r) => r.productId && r.adjustQuantity); + if (validDetails.length === 0) { + message.error('请至少添加一条调整明细'); + return; + } + setSubmitting(true); + try { + const res = await createStockAdjust({ + ...values, + details: validDetails.map((r) => ({ productId: r.productId!, adjustQuantity: r.adjustQuantity! })), + }); + if (res?.code !== 0) { message.error(res?.msg || '创建失败'); return; } + setOpen(false); + actionRef.current?.reload(); + message.success('调整单已创建'); + } catch { + message.error('创建失败'); + } finally { + setSubmitting(false); + } + }; + + const addRow = () => setDetails((prev) => [...prev, { key: rowKey++ }]); + const removeRow = (idx: number) => setDetails((prev) => prev.filter((_, i) => i !== idx)); + const updateRow = (idx: number, field: keyof DetailRow, value: string) => + setDetails((prev) => prev.map((r, i) => (i === idx ? { ...r, [field]: value } : r))); + + const today = new Date().toISOString().slice(0, 10); + + return ( + <> + + actionRef={actionRef} + headerTitle="库存调整" + rowKey="adjustId" + columns={columns} + search={false} + request={async (params) => { + const res = await listStockAdjusts({ page: params.current, pageSize: params.pageSize }); + return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 }; + }} + toolBarRender={() => [ + } onClick={handleOpen}>创建调整单, + ]} + /> + + setOpen(false)} + confirmLoading={submitting} + width={680} + afterOpenChange={(o) => { if (!o) form.resetFields(); }} + > + + + + + + + + + + + + + + + + + + + + 调整明细 + } onClick={addRow}>添加行 + + ( + ({ value: p.productId, label: `${p.productName}${p.spec ? ' ' + p.spec : ''}${p.color ? ' ' + p.color : ''}` }))} + onChange={(v) => updateRow(idx, 'productId', v)} + /> + ), + }, + { + title: '调整数量(米)', + width: 150, + render: (_, __, idx) => ( + updateRow(idx, 'adjustQuantity', e.target.value)} + /> + ), + }, + { + title: '', + width: 40, + render: (_, __, idx) => ( + } + disabled={details.length === 1} + onClick={() => removeRow(idx)} + /> + ), + }, + ]} + /> + + > + ); +}; export default AdjustsPage; diff --git a/src/pages/Inventory/Bolts/index.test.tsx b/src/pages/Inventory/Bolts/index.test.tsx new file mode 100644 index 0000000..ca5df6b --- /dev/null +++ b/src/pages/Inventory/Bolts/index.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Inventory / Bolts page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 匹维度库存 table header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('匹维度库存')).toBeInTheDocument(); + }); + + it('renders 长度(m) column header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('长度(m)')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Inventory/Checks/detail.tsx b/src/pages/Inventory/Checks/detail.tsx new file mode 100644 index 0000000..8326e40 --- /dev/null +++ b/src/pages/Inventory/Checks/detail.tsx @@ -0,0 +1,94 @@ +import { ArrowLeftOutlined } from '@ant-design/icons'; +import { ProTable } from '@ant-design/pro-components'; +import { Button, Card, Descriptions, Modal, Space, Tag, Typography, message } from 'antd'; +import { useEffect, useState } from 'react'; +import { history, useParams } from '@umijs/max'; +import { confirmStockCheck, getStockCheck } from '@/services/api'; +import type { StockCheck } from '@/types/api'; + +const STATUS_MAP: Record = { + 0: { text: '草稿', color: 'default' }, + 1: { text: '进行中', color: 'processing' }, + 2: { text: '已完成', color: 'success' }, +}; + +const CheckDetailPage: React.FC = () => { + const { id } = useParams<{ id: string }>(); + const [check, setCheck] = useState(null); + + const load = async () => { + if (!id) return; + const res = await getStockCheck(id); + if (res?.code === 0) setCheck(res.data as StockCheck); + }; + + useEffect(() => { load(); }, [id]); + + const handleConfirm = () => { + Modal.confirm({ + title: '确认开始盘点?', + onOk: async () => { + await confirmStockCheck(id!); + message.success('已确认'); + load(); + }, + }); + }; + + if (!check) return null; + + const status = STATUS_MAP[check.status] ?? STATUS_MAP[0]; + + return ( + + + } onClick={() => history.back()}>返回 + 盘点单详情 — {check.checkNo} + + + + + {check.checkNo} + {check.checkDate} + {check.checker || '-'} + + {status.text} + + {check.createdAt} + {check.remark && {check.remark}} + + {check.status === 0 && ( + 确认开始盘点 + )} + + + + { + const n = parseFloat(v); + const color = n > 0 ? '#52c41a' : n < 0 ? '#ff4d4f' : undefined; + return {v}; + }, + }, + { title: '差异金额', dataIndex: 'diffAmount', render: (v: string) => `¥ ${v}` }, + { title: '备注', dataIndex: 'remark', render: (v: string) => v || '-' }, + ]} + /> + + + ); +}; + +export default CheckDetailPage; diff --git a/src/pages/Inventory/Checks/index.test.tsx b/src/pages/Inventory/Checks/index.test.tsx new file mode 100644 index 0000000..10fd75a --- /dev/null +++ b/src/pages/Inventory/Checks/index.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Inventory / Checks page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 创建盘点单 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /创建盘点单/ })).toBeInTheDocument(); + }); + + it('renders 库存盘点 table header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('库存盘点')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Inventory/Checks/index.tsx b/src/pages/Inventory/Checks/index.tsx index 8ee51ec..81f76bf 100644 --- a/src/pages/Inventory/Checks/index.tsx +++ b/src/pages/Inventory/Checks/index.tsx @@ -1,8 +1,10 @@ -import { PlusOutlined } from '@ant-design/icons'; -import { ProColumns, ProTable } from '@ant-design/pro-components'; -import { Button, Tag } from 'antd'; -import { listStockChecks } from '@/services/api'; -import type { StockCheck } from '@/types/api'; +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { ActionType, ProColumns, ProTable } from '@ant-design/pro-components'; +import { Button, Col, Form, Input, Modal, Row, Select, Table, Tag, message } from 'antd'; +import { useEffect, useRef, useState } from 'react'; +import { Link } from '@umijs/max'; +import { createStockCheck, listProducts, listStockChecks } from '@/services/api'; +import type { Product, StockCheck } from '@/types/api'; const STATUS_MAP: Record = { 0: { text: '草稿', color: 'default' }, @@ -10,37 +12,170 @@ const STATUS_MAP: Record = { 2: { text: '已完成', color: 'success' }, }; -const columns: ProColumns[] = [ - { title: '盘点单号', dataIndex: 'checkNo' }, - { title: '盘点日期', dataIndex: 'checkDate', valueType: 'date', search: false }, - { title: '盘点人', dataIndex: 'checker', search: false }, - { - title: '状态', - dataIndex: 'status', - search: false, - render: (_, record) => { - const s = STATUS_MAP[record.status] ?? STATUS_MAP[0]; - return {s.text}; - }, - }, - { title: '创建时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false }, - { title: '操作', valueType: 'option', render: () => [查看] }, -]; +interface DetailRow { key: number; productId?: string; actualQuantity?: string } +let rowKey = 0; -const ChecksPage: React.FC = () => ( - - headerTitle="库存盘点" - rowKey="checkId" - columns={columns} - search={false} - request={async (params) => { - const res = await listStockChecks({ page: params.current, pageSize: params.pageSize }); - return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 }; - }} - toolBarRender={() => [ - }>创建盘点单, - ]} - /> -); +const ChecksPage: React.FC = () => { + const columns: ProColumns[] = [ + { title: '盘点单号', dataIndex: 'checkNo' }, + { title: '盘点日期', dataIndex: 'checkDate', valueType: 'date', search: false }, + { title: '盘点人', dataIndex: 'checker', search: false }, + { + title: '状态', + dataIndex: 'status', + search: false, + render: (_, record) => { + const s = STATUS_MAP[record.status] ?? STATUS_MAP[0]; + return {s.text}; + }, + }, + { title: '创建时间', dataIndex: 'createdAt', valueType: 'dateTime', search: false }, + { title: '操作', valueType: 'option', render: (_, record) => [查看] }, + ]; + const actionRef = useRef(); + const [form] = Form.useForm(); + const [open, setOpen] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [products, setProducts] = useState([]); + const [details, setDetails] = useState([]); + + useEffect(() => { + listProducts({ page: 1, pageSize: 500 }).then((res) => { + if (res?.code === 0) setProducts(res.data?.list || []); + }); + }, []); + + const handleOpen = () => { + setDetails([]); + setOpen(true); + }; + + const handleSubmit = async () => { + let values: { checkDate: string; checker?: string; remark?: string }; + try { + values = await form.validateFields(); + } catch { + return; + } + setSubmitting(true); + try { + const res = await createStockCheck({ + ...values, + details: details + .filter((r) => r.productId && r.actualQuantity) + .map((r) => ({ productId: r.productId!, actualQuantity: r.actualQuantity! })), + }); + if (res?.code !== 0) { message.error(res?.msg || '创建失败'); return; } + setOpen(false); + actionRef.current?.reload(); + message.success('盘点单已创建'); + } catch { + message.error('创建失败'); + } finally { + setSubmitting(false); + } + }; + + const addRow = () => setDetails((prev) => [...prev, { key: rowKey++ }]); + const removeRow = (idx: number) => setDetails((prev) => prev.filter((_, i) => i !== idx)); + const updateRow = (idx: number, field: keyof DetailRow, value: string) => + setDetails((prev) => prev.map((r, i) => (i === idx ? { ...r, [field]: value } : r))); + + const today = new Date().toISOString().slice(0, 10); + + return ( + <> + + actionRef={actionRef} + headerTitle="库存盘点" + rowKey="checkId" + columns={columns} + search={false} + request={async (params) => { + const res = await listStockChecks({ page: params.current, pageSize: params.pageSize }); + return { data: res?.data?.list || [], total: res?.data?.total || 0, success: res?.code === 0 }; + }} + toolBarRender={() => [ + } onClick={handleOpen}>创建盘点单, + ]} + /> + + setOpen(false)} + confirmLoading={submitting} + width={680} + afterOpenChange={(o) => { if (!o) form.resetFields(); }} + > + + + + + + + + + + + + + + + + + + + + 盘点明细(可选) + } onClick={addRow}>添加行 + + ( + ({ value: p.productId, label: `${p.productName}${p.spec ? ' ' + p.spec : ''}${p.color ? ' ' + p.color : ''}` }))} + onChange={(v) => updateRow(idx, 'productId', v)} + /> + ), + }, + { + title: '实盘数量(米)', + width: 140, + render: (_, __, idx) => ( + updateRow(idx, 'actualQuantity', e.target.value)} + /> + ), + }, + { + title: '', + width: 40, + render: (_, __, idx) => ( + } onClick={() => removeRow(idx)} /> + ), + }, + ]} + /> + + > + ); +}; export default ChecksPage; diff --git a/src/pages/Inventory/Import/index.test.tsx b/src/pages/Inventory/Import/index.test.tsx new file mode 100644 index 0000000..90f2378 --- /dev/null +++ b/src/pages/Inventory/Import/index.test.tsx @@ -0,0 +1,43 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Inventory / Import page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 下载导入模板 button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /下载导入模板/ })).toBeInTheDocument(); + }); + + it('renders upload dragger with instructional text', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText(/点击或拖拽文件到此处上传/)).toBeInTheDocument(); + }); + + it('renders import instructions alert', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('导入说明')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Inventory/Pans/index.test.tsx b/src/pages/Inventory/Pans/index.test.tsx new file mode 100644 index 0000000..d8500c6 --- /dev/null +++ b/src/pages/Inventory/Pans/index.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Inventory / Pans page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 盘维度库存 table header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('盘维度库存')).toBeInTheDocument(); + }); + + it('renders 匹数 column header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('匹数')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Inventory/Products/index.test.tsx b/src/pages/Inventory/Products/index.test.tsx new file mode 100644 index 0000000..7faf4c3 --- /dev/null +++ b/src/pages/Inventory/Products/index.test.tsx @@ -0,0 +1,151 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; +import { createProduct, getProductSummary } from '@/services/api'; + +describe('Inventory / Products page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 新增产品 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /新增产品/ })).toBeInTheDocument(); + }); + + it('opens create dialog when 新增产品 is clicked', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + expect(within(screen.getByRole('dialog')).getByText('新增产品')).toBeInTheDocument(); + }); + + it('calls createProduct after filling form and submitting', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + await waitFor(() => screen.getByRole('dialog')); + + const dialog = screen.getByRole('dialog'); + const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ }); + await user.type(nameInput, '测试产品'); + + // Click the submit/OK button – find the non-close button in the dialog footer + const buttons = within(dialog).getAllByRole('button'); + const submitBtn = buttons.find((b) => { + const text = b.textContent?.trim() ?? ''; + return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text); + }); + if (submitBtn) await user.click(submitBtn); + + await waitFor(() => { + expect(vi.mocked(createProduct)).toHaveBeenCalled(); + }); + }); + + it('renders Excel export button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /Excel导出/ })).toBeInTheDocument(); + }); + + // Regression: when a product has no pans the backend returns empty strings for + // colors/locations (via COALESCE). Previously GROUP_CONCAT returned NULL which + // crashed the Go scanner and made the entire summary return an error, leaving the + // table empty. This test ensures the frontend handles the fixed response correctly. + it('renders summary table without crash when product has empty colors and locations', async () => { + vi.mocked(getProductSummary).mockResolvedValueOnce({ + code: 0, + msg: 'ok', + data: { + list: [ + { + productName: '无盘测试布料', + spec: '100cm', + colorCount: 1, + totalPanCount: 0, + totalBoltCount: 0, + totalLengthM: '0.00', + colors: '', + locations: '', + }, + ], + }, + } as any); + + render(); + + // ProTable header text appears in multiple nodes — presence check is enough + await waitFor(() => { + expect(screen.getAllByText('产品总览').length).toBeGreaterThan(0); + }); + // getProductSummary was invoked (not short-circuited by an error) + await waitFor(() => { + expect(vi.mocked(getProductSummary)).toHaveBeenCalled(); + }); + }); + + // Verifies the success path: createProduct returns code 0 → ModalForm closes. + // The dialog closing is the proxy for summaryRef.reload() being reached — + // ModalForm only calls onOpenChange(false) when onFinish returns true, + // which is the same branch that triggers the summary reload. + it('closes dialog after successful product creation (reload branch reached)', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增产品/ })); + await waitFor(() => screen.getByRole('dialog')); + + const dialog = screen.getByRole('dialog'); + const nameInput = within(dialog).getByRole('textbox', { name: /产品名称/ }); + await user.type(nameInput, '新布料'); + + const buttons = within(dialog).getAllByRole('button'); + const submitBtn = buttons.find((b) => { + const text = b.textContent?.trim() ?? ''; + return text && !text.includes('×') && !text.includes('✕') && !/取/.test(text) && !/重/.test(text) && !/Cancel/i.test(text); + }); + if (submitBtn) await user.click(submitBtn); + + await waitFor(() => { + expect(vi.mocked(createProduct)).toHaveBeenCalled(); + }); + // ModalForm closes only when onFinish returns true — the same branch that calls reload() + await waitFor(() => { + expect(screen.queryByRole('dialog')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/src/pages/Inventory/Stats/index.test.tsx b/src/pages/Inventory/Stats/index.test.tsx new file mode 100644 index 0000000..c0a0efa --- /dev/null +++ b/src/pages/Inventory/Stats/index.test.tsx @@ -0,0 +1,33 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Inventory / Stats page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('displays stat card titles after data loads', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('产品总数')).toBeInTheDocument(); + }); + expect(screen.getByText('总匹数')).toBeInTheDocument(); + expect(screen.getByText('总盘数')).toBeInTheDocument(); + }); + + it('renders color and location breakdown tables', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('按颜色分类统计')).toBeInTheDocument(); + }); + expect(screen.getByText('按位置分布统计')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Inventory/Stocks/index.test.tsx b/src/pages/Inventory/Stocks/index.test.tsx new file mode 100644 index 0000000..6b07b8c --- /dev/null +++ b/src/pages/Inventory/Stocks/index.test.tsx @@ -0,0 +1,36 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Inventory / Stocks page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 库存查询 table header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('库存查询')).toBeInTheDocument(); + }); + + it('renders column headers', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + // These texts appear in both the search form and the table column header + expect(screen.getAllByText('产品名称').length).toBeGreaterThan(0); + expect(screen.getAllByText('颜色').length).toBeGreaterThan(0); + }); +}); diff --git a/src/pages/Login/index.test.tsx b/src/pages/Login/index.test.tsx new file mode 100644 index 0000000..40f20a4 --- /dev/null +++ b/src/pages/Login/index.test.tsx @@ -0,0 +1,54 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../test/smoke'; +import Page from './index'; + +describe('Login page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders username and password inputs', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByPlaceholderText('用户名')).toBeInTheDocument(); + expect(screen.getByPlaceholderText('密码')).toBeInTheDocument(); + }); + + it('renders login submit button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + // antd Form buttons are not exposed in the a11y tree in jsdom; query DOM directly + expect(document.querySelector('button[type="submit"]')).toBeInTheDocument(); + }); + + it('shows validation errors when form submitted without values', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + const submitBtn = document.querySelector('button[type="submit"]') as HTMLElement; + await user.click(submitBtn); + + await waitFor(() => { + expect(screen.getByText('请输入用户名')).toBeInTheDocument(); + }); + expect(screen.getByText('请输入密码')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Purchase/Orders/detail.test.tsx b/src/pages/Purchase/Orders/detail.test.tsx new file mode 100644 index 0000000..a1c35b4 --- /dev/null +++ b/src/pages/Purchase/Orders/detail.test.tsx @@ -0,0 +1,77 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './detail'; +import { getPurchaseOrder } from '@/services/api'; + +describe('Purchase / Orders / detail page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders order detail sections when order data is available', async () => { + vi.mocked(getPurchaseOrder).mockResolvedValueOnce({ + code: 0, + msg: 'ok', + success: true, + data: { + orderId: 'order_001', + orderNo: 'PO-2024-001', + supplierName: '测试供应商', + orderDate: '2024-01-15', + purchaseBy: '张三', + contractAmount: '10000', + receivedQty: '0', + paidAmount: '0', + receiptStatus: 0, + paymentStatus: 0, + status: 0, + remark: '', + details: [], + } as any, + }); + + render(); + + await waitFor(() => { + // Text is embedded inside "采购订单详情 — PO-2024-001", use regex match + expect(screen.getByText(/PO-2024-001/)).toBeInTheDocument(); + }); + expect(screen.getByText('测试供应商')).toBeInTheDocument(); + }); + + it('shows 确认订单 button for draft status orders', async () => { + vi.mocked(getPurchaseOrder).mockResolvedValueOnce({ + code: 0, + msg: 'ok', + success: true, + data: { + orderId: 'order_001', + orderNo: 'PO-2024-001', + supplierName: '供应商A', + orderDate: '2024-01-15', + purchaseBy: '张三', + contractAmount: '5000', + receivedQty: '0', + paidAmount: '0', + receiptStatus: 0, + paymentStatus: 0, + status: 0, + remark: '', + details: [], + } as any, + }); + + render(); + + await waitFor(() => { + expect(screen.getByRole('button', { name: /确认订单/ })).toBeInTheDocument(); + }); + }); +}); diff --git a/src/pages/Purchase/Orders/index.test.tsx b/src/pages/Purchase/Orders/index.test.tsx new file mode 100644 index 0000000..af1f3ba --- /dev/null +++ b/src/pages/Purchase/Orders/index.test.tsx @@ -0,0 +1,49 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { history } from '@umijs/max'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Purchase / Orders page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 创建采购订单 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /创建采购订单/ })).toBeInTheDocument(); + }); + + it('navigates to new order page on 创建采购订单 click', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /创建采购订单/ })); + + expect(history.push).toHaveBeenCalledWith('/purchase/orders/new'); + }); + + it('renders 采购订单 table header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('采购订单')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Purchase/Orders/new.tsx b/src/pages/Purchase/Orders/new.tsx new file mode 100644 index 0000000..2ea321b --- /dev/null +++ b/src/pages/Purchase/Orders/new.tsx @@ -0,0 +1,243 @@ +import { DeleteOutlined, PlusOutlined } from '@ant-design/icons'; +import { Button, Card, Col, Form, Input, Row, Select, Space, Table, Typography, message } from 'antd'; +import { useEffect, useState } from 'react'; +import { history } from '@umijs/max'; +import { createPurchaseOrder, listProducts, listSuppliers } from '@/services/api'; +import type { Product, Supplier } from '@/types/api'; + +const { Title } = Typography; + +interface DetailRow { + key: number; + productId?: string; + productName?: string; + spec?: string; + color?: string; + quantity?: string; + unitPrice?: string; +} + +let rowKey = 0; + +const NewOrderPage: React.FC = () => { + const [form] = Form.useForm(); + const [suppliers, setSuppliers] = useState([]); + const [products, setProducts] = useState([]); + const [details, setDetails] = useState([{ key: rowKey++ }]); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + listSuppliers({ page: 1, pageSize: 200, status: 1 }).then((res) => { + if (res?.code === 0) setSuppliers(res.data?.list || []); + }); + listProducts({ page: 1, pageSize: 500 }).then((res) => { + if (res?.code === 0) setProducts(res.data?.list || []); + }); + }, []); + + const productMap = Object.fromEntries(products.map((p) => [p.productId, p])); + + const handleProductChange = (rowIdx: number, productId: string) => { + const p = productMap[productId]; + if (!p) return; + setDetails((prev) => + prev.map((r, i) => + i === rowIdx + ? { ...r, productId, productName: p.productName, spec: p.spec || '', color: p.color || '' } + : r, + ), + ); + }; + + const handleFieldChange = (rowIdx: number, field: keyof DetailRow, value: string) => { + setDetails((prev) => prev.map((r, i) => (i === rowIdx ? { ...r, [field]: value } : r))); + }; + + const addRow = () => setDetails((prev) => [...prev, { key: rowKey++ }]); + + const removeRow = (rowIdx: number) => { + setDetails((prev) => prev.filter((_, i) => i !== rowIdx)); + }; + + const calcAmount = (row: DetailRow) => { + const q = parseFloat(row.quantity || '0'); + const p = parseFloat(row.unitPrice || '0'); + return isNaN(q) || isNaN(p) ? '' : (q * p).toFixed(2); + }; + + const handleSubmit = async () => { + let headerValues: { supplierId: string; orderDate: string; purchaseBy?: string; remark?: string }; + try { + headerValues = await form.validateFields(); + } catch { + return; + } + + const validDetails = details.filter((r) => r.productId && r.quantity && r.unitPrice); + if (validDetails.length === 0) { + message.error('请至少添加一条采购明细'); + return; + } + + setSubmitting(true); + try { + const res = await createPurchaseOrder({ + ...headerValues, + details: validDetails.map((r) => ({ + productId: r.productId!, + productName: r.productName!, + spec: r.spec, + color: r.color, + quantity: r.quantity!, + unitPrice: r.unitPrice!, + })), + }); + if (res?.code !== 0) { + message.error(res?.msg || '创建失败'); + return; + } + message.success('采购订单已创建'); + history.push(`/purchase/orders/${res.data.id}`); + } catch { + message.error('创建失败'); + } finally { + setSubmitting(false); + } + }; + + const columns = [ + { + title: '产品', + width: 220, + render: (_: unknown, _row: DetailRow, idx: number) => ( + ({ value: p.productId, label: `${p.productName}${p.spec ? ` ${p.spec}` : ''}${p.color ? ` ${p.color}` : ''}` }))} + onChange={(v) => handleProductChange(idx, v)} + /> + ), + }, + { + title: '规格', + width: 120, + render: (_: unknown, _row: DetailRow, idx: number) => ( + handleFieldChange(idx, 'spec', e.target.value)} /> + ), + }, + { + title: '颜色', + width: 120, + render: (_: unknown, _row: DetailRow, idx: number) => ( + handleFieldChange(idx, 'color', e.target.value)} /> + ), + }, + { + title: '采购数量', + width: 120, + render: (_: unknown, _row: DetailRow, idx: number) => ( + handleFieldChange(idx, 'quantity', e.target.value)} + /> + ), + }, + { + title: '单价', + width: 120, + render: (_: unknown, _row: DetailRow, idx: number) => ( + handleFieldChange(idx, 'unitPrice', e.target.value)} + /> + ), + }, + { + title: '小计', + width: 100, + render: (_: unknown, _row: DetailRow, idx: number) => calcAmount(details[idx]), + }, + { + title: '', + width: 48, + render: (_: unknown, _row: DetailRow, idx: number) => ( + } + disabled={details.length === 1} + onClick={() => removeRow(idx)} + /> + ), + }, + ]; + + const today = new Date().toISOString().slice(0, 10); + + return ( + + + history.back()}>返回 + 新建采购订单 + + + + + + + + ({ value: s.supplierId, label: s.supplierName }))} + /> + + + + + + + + + + + + + + + + + + + + } onClick={addRow}>添加明细} + > + + + + + history.back()}>取消 + 提交 + + + ); +}; + +export default NewOrderPage; diff --git a/src/pages/Purchase/Receipts/index.test.tsx b/src/pages/Purchase/Receipts/index.test.tsx new file mode 100644 index 0000000..345ce3f --- /dev/null +++ b/src/pages/Purchase/Receipts/index.test.tsx @@ -0,0 +1,35 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Purchase / Receipts page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 入库记录 table header', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('入库记录')).toBeInTheDocument(); + }); + + it('renders status and receipt date column headers', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByText('入库单号')).toBeInTheDocument(); + expect(screen.getByText('状态')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Purchase/Stats/index.test.tsx b/src/pages/Purchase/Stats/index.test.tsx new file mode 100644 index 0000000..033a316 --- /dev/null +++ b/src/pages/Purchase/Stats/index.test.tsx @@ -0,0 +1,42 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('Purchase / Stats page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 采购报表 heading', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('采购报表')).toBeInTheDocument(); + }); + }); + + it('renders summary stat cards', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('采购订单数')).toBeInTheDocument(); + }); + // '采购总额' and '已付款' appear in both stat cards and ProTable column headers + expect(screen.getAllByText('采购总额').length).toBeGreaterThan(0); + expect(screen.getAllByText('已付款').length).toBeGreaterThan(0); + expect(screen.getByText('未付款')).toBeInTheDocument(); + }); + + it('renders breakdown table sections', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('按供应商统计')).toBeInTheDocument(); + }); + expect(screen.getByText('按产品统计')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/Purchase/Suppliers/index.test.tsx b/src/pages/Purchase/Suppliers/index.test.tsx new file mode 100644 index 0000000..cd4c956 --- /dev/null +++ b/src/pages/Purchase/Suppliers/index.test.tsx @@ -0,0 +1,82 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act, within, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; +import { createSupplier } from '@/services/api'; + +describe('Purchase / Suppliers page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 新增供应商 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /新增供应商/ })).toBeInTheDocument(); + }); + + it('opens create modal on 新增供应商 click', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增供应商/ })); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + expect(within(screen.getByRole('dialog')).getByText('新增供应商')).toBeInTheDocument(); + }); + + it('calls createSupplier after filling and submitting form', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增供应商/ })); + + // Wait for the modal and its content to be accessible + await waitFor( + () => { + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getAllByRole('button').length).toBeGreaterThan(1); + }, + { timeout: 5000 }, + ); + + const dialog = screen.getByRole('dialog') as HTMLElement; + + // antd Form.Item + Input is not exposed in the a11y tree; use DOM query + await waitFor(() => expect(dialog.querySelector('input')).not.toBeNull(), { timeout: 3000 }); + const nameInput = dialog.querySelector('input') as HTMLInputElement; + fireEvent.change(nameInput, { target: { value: '新供应商' } }); + + // The modal footer OK button IS accessible (it is outside the Form element) + const buttons = within(dialog).getAllByRole('button'); + const okBtn = buttons.find((b) => { + const text = b.textContent?.replace(/\s/g, '') ?? ''; + return text.includes('确定') || text === 'OK'; + }); + if (okBtn) await user.click(okBtn); + + await waitFor(() => { + expect(vi.mocked(createSupplier)).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pages/Purchase/Suppliers/index.tsx b/src/pages/Purchase/Suppliers/index.tsx index 26aea6c..8df0146 100644 --- a/src/pages/Purchase/Suppliers/index.tsx +++ b/src/pages/Purchase/Suppliers/index.tsx @@ -30,15 +30,24 @@ const SuppliersPage: React.FC = () => { const handleSubmit = async () => { const values = await form.validateFields(); - if (editing) { - await updateSupplier(editing.supplierId, values); - message.success('更新成功'); - } else { - await createSupplier(values); - message.success('创建成功'); + let successMsg = ''; + try { + if (editing) { + const res = await updateSupplier(editing.supplierId, values); + if (res?.code !== 0) { message.error(res?.msg || '更新失败'); return; } + successMsg = '更新成功'; + } else { + const res = await createSupplier(values); + if (res?.code !== 0) { message.error(res?.msg || '创建失败'); return; } + successMsg = '创建成功'; + } + } catch { + message.error('操作失败'); + return; } setModalOpen(false); actionRef.current?.reload(); + message.success(successMsg); }; const handleDelete = async (id: string) => { @@ -103,7 +112,7 @@ const SuppliersPage: React.FC = () => { open={modalOpen} onOk={handleSubmit} onCancel={() => setModalOpen(false)} - destroyOnClose + afterOpenChange={(open) => { if (!open) form.resetFields(); }} > diff --git a/src/pages/System/Configs/index.test.tsx b/src/pages/System/Configs/index.test.tsx new file mode 100644 index 0000000..072cdec --- /dev/null +++ b/src/pages/System/Configs/index.test.tsx @@ -0,0 +1,60 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; +import { listConfigs } from '@/services/api'; + +describe('System / Configs page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders config table column headers', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('配置名称')).toBeInTheDocument(); + }); + expect(screen.getByText('配置键')).toBeInTheDocument(); + expect(screen.getByText('配置值')).toBeInTheDocument(); + }); + + it('shows inline edit input after clicking 编辑', async () => { + vi.mocked(listConfigs).mockResolvedValueOnce({ + code: 0, + msg: 'ok', + success: true, + data: { + list: [ + { + configKey: 'site_name', + configName: '站点名称', + configValue: '木羽系统', + configGroup: 'system', + remark: '站点显示名称', + }, + ], + total: 1, + } as any, + }); + + const user = userEvent.setup(); + render(); + + await waitFor(() => { + expect(screen.getByText('站点名称')).toBeInTheDocument(); + }); + + await user.click(screen.getByText('编辑')); + + await waitFor(() => { + expect(screen.getByDisplayValue('木羽系统')).toBeInTheDocument(); + }); + }); +}); diff --git a/src/pages/System/Menus/index.test.tsx b/src/pages/System/Menus/index.test.tsx new file mode 100644 index 0000000..f8f86a9 --- /dev/null +++ b/src/pages/System/Menus/index.test.tsx @@ -0,0 +1,34 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act } from '@testing-library/react'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; + +describe('System / Menus page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 新增菜单 button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /新增菜单/ })).toBeInTheDocument(); + }); + + it('renders menu table column headers', async () => { + render(); + await waitFor(() => { + expect(screen.getByText('菜单名称')).toBeInTheDocument(); + }); + expect(screen.getByText('路由')).toBeInTheDocument(); + expect(screen.getByText('类型')).toBeInTheDocument(); + }); +}); diff --git a/src/pages/System/Roles/index.test.tsx b/src/pages/System/Roles/index.test.tsx new file mode 100644 index 0000000..54626e5 --- /dev/null +++ b/src/pages/System/Roles/index.test.tsx @@ -0,0 +1,78 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act, within, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; +import { createRole } from '@/services/api'; + +describe('System / Roles page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 新增角色 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /新增角色/ })).toBeInTheDocument(); + }); + + it('opens create dialog on 新增角色 click', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增角色/ })); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + expect(within(screen.getByRole('dialog')).getByText('新增角色')).toBeInTheDocument(); + }); + + it('calls createRole after filling required fields and submitting', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增角色/ })); + + // Wait until the modal content is fully accessible (footer buttons appear alongside Close) + await waitFor( + () => { + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getAllByRole('button').length).toBeGreaterThan(1); + }, + { timeout: 5000 }, + ); + + const dialog = screen.getByRole('dialog') as HTMLElement; + // Use index-based access: duplicate form IDs from ProTable search break name-based lookup + // fireEvent.change is more reliable than user.type for antd controlled ProForm inputs + const textboxes = within(dialog).getAllByRole('textbox'); + fireEvent.change(textboxes[0], { target: { value: '测试角色' } }); + if (textboxes[1]) fireEvent.change(textboxes[1], { target: { value: 'test_role' } }); + + // Click the primary button in the modal footer (the Submit/OK button) + const submitBtn = dialog.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement | null; + if (submitBtn) fireEvent.click(submitBtn); + + await waitFor(() => { + expect(vi.mocked(createRole)).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pages/System/Users/index.test.tsx b/src/pages/System/Users/index.test.tsx new file mode 100644 index 0000000..0b04d28 --- /dev/null +++ b/src/pages/System/Users/index.test.tsx @@ -0,0 +1,82 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { render, screen, waitFor, act, within, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +vi.mock('@umijs/max'); +vi.mock('@/services/api'); + +import { smokeTest } from '../../../../test/smoke'; +import Page from './index'; +import { createUser } from '@/services/api'; + +describe('System / Users page', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + smokeTest(Page); + + it('renders 新增用户 toolbar button', async () => { + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole('button', { name: /新增用户/ })).toBeInTheDocument(); + }); + + it('opens create dialog on 新增用户 click', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增用户/ })); + + await waitFor(() => { + expect(screen.getByRole('dialog')).toBeInTheDocument(); + }); + expect(within(screen.getByRole('dialog')).getByText('新增用户')).toBeInTheDocument(); + }); + + it('calls createUser after filling required fields and submitting', async () => { + const user = userEvent.setup(); + render(); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + + await user.click(screen.getByRole('button', { name: /新增用户/ })); + + // Wait until modal content is fully accessible (footer buttons appear beyond Close) + await waitFor( + () => { + const dialog = screen.getByRole('dialog'); + expect(within(dialog).getAllByRole('button').length).toBeGreaterThan(1); + }, + { timeout: 5000 }, + ); + + const dialog = screen.getByRole('dialog') as HTMLElement; + // Use index-based access; duplicate form IDs from ProTable search break name-based lookup + // fireEvent.change is more reliable for antd controlled ProForm inputs + const textboxes = within(dialog).getAllByRole('textbox'); + fireEvent.change(textboxes[0], { target: { value: 'testuser' } }); + if (textboxes[1]) fireEvent.change(textboxes[1], { target: { value: '测试用户' } }); + + // Fill password field (type=password) using DOM query + const passwordInput = dialog.querySelector('input[type="password"]') as HTMLInputElement | null; + if (passwordInput) fireEvent.change(passwordInput, { target: { value: 'Password123' } }); + + // Click the primary button in the modal footer (Submit/OK) + const submitBtn = dialog.querySelector('.ant-modal-footer .ant-btn-primary') as HTMLElement | null; + if (submitBtn) fireEvent.click(submitBtn); + + await waitFor(() => { + expect(vi.mocked(createUser)).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/pages/System/Users/index.tsx b/src/pages/System/Users/index.tsx index 1deb5a3..5cd9fbc 100644 --- a/src/pages/System/Users/index.tsx +++ b/src/pages/System/Users/index.tsx @@ -100,7 +100,7 @@ const UsersPage: React.FC = () => { - + Promise.resolve({ code: 0, msg: "ok", success: true, data: makeData() }); + +export const login = vi.fn(() => + Promise.resolve({ code: 0, msg: "ok", data: { token: "test-token", userInfo: { tenantId: "t1", name: "tester" } } }), +); +export const getUserInfo = vi.fn(() => + Promise.resolve({ code: 0, msg: "ok", data: { name: "tester", menuPaths: [], roles: [] } }), +); +export const logout = vi.fn(ok); +export const changePassword = vi.fn(ok); +export const listUsers = vi.fn(ok); +export const getUser = vi.fn(ok); +export const createUser = vi.fn(ok); +export const updateUser = vi.fn(ok); +export const deleteUser = vi.fn(ok); +export const updateUserStatus = vi.fn(ok); +export const listRoles = vi.fn(ok); +export const createRole = vi.fn(ok); +export const updateRole = vi.fn(ok); +export const deleteRole = vi.fn(ok); +export const setRolePermissions = vi.fn(ok); +export const listMenus = vi.fn(ok); +export const createMenu = vi.fn(ok); +export const updateMenu = vi.fn(ok); +export const deleteMenu = vi.fn(ok); +export const listLogs = vi.fn(ok); +export const listConfigs = vi.fn(ok); +export const updateConfig = vi.fn(ok); +export const listProducts = vi.fn(ok); +export const getProduct = vi.fn(ok); +export const createProduct = vi.fn(ok); +export const updateProduct = vi.fn(ok); +export const deleteProduct = vi.fn(ok); +export const exportProducts = vi.fn(ok); +export const getProductSummary = vi.fn(ok); +export const getColorDetail = vi.fn(ok); +export const getProductTree = vi.fn(ok); +export const listPans = vi.fn(ok); +export const savePans = vi.fn(ok); +export const listBolts = vi.fn(ok); +export const saveBolts = vi.fn(ok); +export const listPanView = vi.fn(ok); +export const listBoltView = vi.fn(ok); +export const getStockSummary = vi.fn(ok); +export const getStockByColor = vi.fn(ok); +export const getStockByLocation = vi.fn(ok); +export const listStockChecks = vi.fn(ok); +export const getStockCheck = vi.fn(ok); +export const createStockCheck = vi.fn(ok); +export const confirmStockCheck = vi.fn(ok); +export const listStockAdjusts = vi.fn(ok); +export const getStockAdjust = vi.fn(ok); +export const createStockAdjust = vi.fn(ok); +export const approveStockAdjust = vi.fn(ok); +export const listUpstream = vi.fn(ok); +export const listDownstream = vi.fn(ok); +export const createRelation = vi.fn(ok); +export const updateRelation = vi.fn(ok); +export const listRelationHistory = vi.fn(ok); +export const listSuppliers = vi.fn(ok); +export const getSupplier = vi.fn(ok); +export const createSupplier = vi.fn(ok); +export const updateSupplier = vi.fn(ok); +export const deleteSupplier = vi.fn(ok); +export const listPurchaseOrders = vi.fn(ok); +export const getPurchaseOrder = vi.fn(ok); +export const createPurchaseOrder = vi.fn(ok); +export const updatePurchaseOrder = vi.fn(ok); +export const confirmPurchaseOrder = vi.fn(ok); +export const cancelPurchaseOrder = vi.fn(ok); +export const listPurchaseReceipts = vi.fn(ok); +export const getPurchaseReceipt = vi.fn(ok); +export const createPurchaseReceipt = vi.fn(ok); +export const confirmPurchaseReceipt = vi.fn(ok); +export const listPurchasePayments = vi.fn(ok); +export const createPurchasePayment = vi.fn(ok); +export const getPurchaseStatsSummary = vi.fn(ok); +export const getPurchaseStatsBySupplier = vi.fn(ok); +export const getPurchaseStatsByProduct = vi.fn(ok); +export const listInventoryLogs = vi.fn(ok); diff --git a/src/services/api.ts b/src/services/api.ts index fd21f1a..78d23b3 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -200,8 +200,12 @@ export const listStockChecks = (params: PaginationParams) => export const getStockCheck = (id: string) => request>(`/api/v1/inventory/checks/${id}`, { method: 'GET' }); -export const createStockCheck = (data: Partial) => - request('/api/v1/inventory/checks', { method: 'POST', data }); +export const createStockCheck = (data: { + checkDate: string; + checker?: string; + remark?: string; + details?: Array<{ productId: string; actualQuantity: string; remark?: string }>; +}) => request>('/api/v1/inventory/checks', { method: 'POST', data }); export const confirmStockCheck = (id: string) => request(`/api/v1/inventory/checks/${id}/confirm`, { method: 'POST' }); @@ -214,8 +218,12 @@ export const listStockAdjusts = (params: PaginationParams) => export const getStockAdjust = (id: string) => request>(`/api/v1/inventory/adjusts/${id}`, { method: 'GET' }); -export const createStockAdjust = (data: Partial) => - request('/api/v1/inventory/adjusts', { method: 'POST', data }); +export const createStockAdjust = (data: { + adjustDate: string; + adjustReason?: string; + remark?: string; + details?: Array<{ productId: string; adjustQuantity: string; remark?: string }>; +}) => request>('/api/v1/inventory/adjusts', { method: 'POST', data }); export const approveStockAdjust = (id: string, action: number) => request(`/api/v1/inventory/adjusts/${id}/approve`, { method: 'POST', data: { action } }); diff --git a/src/types/api.ts b/src/types/api.ts index 29a38a3..997cd17 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -144,13 +144,39 @@ export interface StockGroup { quantity: number; } +export interface StockCheckDetail { + detailId: string; + checkId: string; + productId: string; + productName: string; + systemQuantity: string; + actualQuantity: string; + diffQuantity: string; + diffAmount: string; + remark?: string; +} + export interface StockCheck { checkId: string; checkNo: string; checkDate: string; checker: string; status: number; + remark?: string; createdAt: string; + updatedAt?: string; + details: StockCheckDetail[]; +} + +export interface StockAdjustDetail { + detailId: string; + adjustId: string; + productId: string; + productName: string; + beforeQuantity: string; + adjustQuantity: string; + afterQuantity: string; + remark?: string; } export interface StockAdjust { @@ -159,8 +185,12 @@ export interface StockAdjust { adjustDate: string; adjustReason: string; operator: string; + approver?: string; status: number; + remark?: string; createdAt: string; + updatedAt?: string; + details: StockAdjustDetail[]; } // ── System ──────────────────────────────────────── diff --git a/test/setup.ts b/test/setup.ts new file mode 100644 index 0000000..790f973 --- /dev/null +++ b/test/setup.ts @@ -0,0 +1,80 @@ +import '@testing-library/jest-dom/vitest'; +import { afterEach, vi } from 'vitest'; +import { cleanup } from '@testing-library/react'; + +// jsdom is missing a handful of browser APIs that antd / pro-components touch on +// mount. Provide minimal stand-ins so component pages can render in tests. + +if (!window.matchMedia) { + window.matchMedia = vi.fn().mockImplementation((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: vi.fn(), + removeListener: vi.fn(), + addEventListener: vi.fn(), + removeEventListener: vi.fn(), + dispatchEvent: vi.fn(), + })); +} + +class ResizeObserverStub { + observe() {} + unobserve() {} + disconnect() {} +} +// @ts-expect-error -- assigning test stub +window.ResizeObserver = window.ResizeObserver || ResizeObserverStub; + +class IntersectionObserverStub { + root = null; + rootMargin = ''; + thresholds = []; + observe() {} + unobserve() {} + disconnect() {} + takeRecords() { + return []; + } +} +// @ts-expect-error -- assigning test stub +window.IntersectionObserver = window.IntersectionObserver || IntersectionObserverStub; + +if (!window.scrollTo) { + // @ts-expect-error -- jsdom does not implement scrollTo + window.scrollTo = vi.fn(); +} + +// Some pages read auth tokens from localStorage during render; ensure a working +// Storage implementation is present regardless of the jsdom build. +if (typeof window.localStorage?.getItem !== 'function') { + const store = new Map(); + const storage: Storage = { + get length() { + return store.size; + }, + clear: () => store.clear(), + getItem: (k: string) => (store.has(k) ? store.get(k)! : null), + key: (i: number) => Array.from(store.keys())[i] ?? null, + removeItem: (k: string) => void store.delete(k), + setItem: (k: string, v: string) => void store.set(k, String(v)), + }; + Object.defineProperty(window, 'localStorage', { value: storage, configurable: true }); +} + +// jsdom's getContext is unimplemented; the CRM graph page guards against a null +// context, so returning null keeps it on its safe path. +if (!HTMLCanvasElement.prototype.getContext) { + HTMLCanvasElement.prototype.getContext = () => null; +} + +// jsdom throws "Not implemented" when getComputedStyle is called with a +// pseudo-element (rc-table does this while measuring scrollbars). Drop the +// unsupported second argument so the call succeeds quietly. +const originalGetComputedStyle = window.getComputedStyle.bind(window); +window.getComputedStyle = ((elt: Element) => + originalGetComputedStyle(elt)) as typeof window.getComputedStyle; + +afterEach(() => { + cleanup(); +}); diff --git a/test/smoke.tsx b/test/smoke.tsx new file mode 100644 index 0000000..c039eb0 --- /dev/null +++ b/test/smoke.tsx @@ -0,0 +1,25 @@ +import { it, expect } from 'vitest'; +import { render, act, cleanup } from '@testing-library/react'; +import type { ComponentType } from 'react'; + +/** + * Smoke test for a routed page entry: render it with its `@umijs/max` and + * `@/services/api` dependencies mocked, flush the async data fetches that + * pages kick off on mount, and assert it mounted without throwing. A render + * error (or an error thrown from a settled effect) fails the test. + * + * Each page test file calls this so every frontend entry has its own case. + */ +export function smokeTest(Page: ComponentType) { + it('mounts without crashing', async () => { + const { container } = render(); + // Let the mocked API promises resolve and their state updates apply + // inside act(), so pages that fetch on mount settle cleanly. + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + expect(container).toBeInstanceOf(HTMLElement); + cleanup(); + }); +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..c81cb4b --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,22 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, 'src'), + '@@': path.resolve(__dirname, 'src/.umi'), + }, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./test/setup.ts'], + include: ['src/**/*.test.{ts,tsx}'], + css: false, + // Pages pull in antd + pro-components; allow a comfortable timeout. + testTimeout: 15000, + }, +});