diff --git a/package-lock.json b/package-lock.json index b5998259..3f9a4ec1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -56,7 +56,7 @@ "jest-environment-jsdom": "^30.3.0", "lint-staged": "^16.4.0", "mockjs": "^1.1.0", - "swagger-ui-dist": "^4.18.1", + "swagger-ui-dist": "^5.32.5", "tailwindcss": "^4.2.4", "ts-node": "^10.9.2", "typescript": "^6.0.3" @@ -6801,6 +6801,14 @@ "react-dom": ">=16.9.0" } }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@simple-libs/child-process-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", @@ -10297,6 +10305,13 @@ "swagger-ui-dist": "^4.18.1" } }, + "node_modules/@umijs/max-plugin-openapi/node_modules/swagger-ui-dist": { + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.1.tgz", + "integrity": "sha512-n/gFn+R7G/BXWwl5UZLw6F1YgWOlf3zkwGlsPhTMhNtAAolBGKg0JS5b2RKt5NI6/hSopVaSrki2wTIMUDDy2w==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@umijs/max/node_modules/@ant-design/colors": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz", @@ -29902,11 +29917,14 @@ } }, "node_modules/swagger-ui-dist": { - "version": "4.19.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.19.1.tgz", - "integrity": "sha512-n/gFn+R7G/BXWwl5UZLw6F1YgWOlf3zkwGlsPhTMhNtAAolBGKg0JS5b2RKt5NI6/hSopVaSrki2wTIMUDDy2w==", + "version": "5.32.5", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.32.5.tgz", + "integrity": "sha512-7/FQfWe9A4qoyYFdAwy0chD0uDYidDp/ZT9VQ9LZlgD4AnnHJk8/+ytAA1HkJYOPySmK6helPDdJQMlcumt7HA==", "dev": true, - "license": "Apache-2.0" + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } }, "node_modules/swagger2openapi": { "version": "7.0.8", diff --git a/package.json b/package.json index b6dfbba4..f24e4971 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,7 @@ "jest-environment-jsdom": "^30.3.0", "lint-staged": "^16.4.0", "mockjs": "^1.1.0", - "swagger-ui-dist": "^4.18.1", + "swagger-ui-dist": "^5.32.5", "tailwindcss": "^4.2.4", "ts-node": "^10.9.2", "typescript": "^6.0.3" diff --git a/src/components/Footer/index.tsx b/src/components/Footer/index.tsx index 1b08bf6d..9f5f0a04 100644 --- a/src/components/Footer/index.tsx +++ b/src/components/Footer/index.tsx @@ -1,6 +1,7 @@ import { GithubOutlined } from '@ant-design/icons'; -import { DefaultFooter } from '@ant-design/pro-components'; import packageJson from '@root/package.json'; +import { Divider } from 'antd'; +import { createStyles } from 'antd-style'; import React from 'react'; const getRepoUrl = () => { @@ -10,66 +11,123 @@ const getRepoUrl = () => { typeof packageJson.repository === 'string' ? packageJson.repository : (packageJson.repository as { url: string }).url; - // Parse git URLs like git@github.com:owner/name.git or https://github.com/owner/name.git const match = repo.match(/github\.com[:/]([^/]+)\/([^/.]+)/); if (!match) return 'https://github.com/ant-design/ant-design-pro'; return `https://github.com/${match[1]}/${match[2]}`; }; const REPO_URL = getRepoUrl(); - -// Git commit hash, resolved at build time from env vars or git const COMMIT_HASH = process.env.COMMIT_HASH || ''; +const useStyles = createStyles(({ token, css }) => ({ + footer: css` + padding: 16px 24px; + text-align: center; + color: ${token.colorTextDescription}; + font-size: ${token.fontSizeSM}px; + line-height: ${token.lineHeight}; + background: transparent; + `, + copyright: css` + margin-bottom: 6px; + `, + link: css` + color: ${token.colorTextDescription}; + text-decoration: none; + transition: color ${token.motionDurationMid}; + + &:hover { + color: ${token.colorText}; + } + `, + meta: css` + display: flex; + align-items: center; + justify-content: center; + flex-wrap: wrap; + gap: 6px 12px; + font-family: ${token.fontFamilyCode}; + font-size: ${token.fontSizeSM - 1}px; + `, + group: css` + display: inline-flex; + align-items: center; + gap: 4px; + `, + label: css` + color: ${token.colorTextQuaternary}; + `, + divider: css` + display: inline-block; + vertical-align: middle; + `, +})); + const Footer: React.FC = () => { + const { styles } = useStyles(); + const year = new Date().getFullYear(); + return ( - - - Ant Design Pro - - ), - href: REPO_URL, - blankTarget: true, - }, - ]} - /> +
+
Ant Design Pro © {year}
+
+ + ver + + {__APP_VERSION__} + + {COMMIT_HASH && ( + + {COMMIT_HASH.slice(0, 7)} + + )} + + + + Umi + + {__UMI_VERSION__} + + + + + Utoo + + {__UTOO_VERSION__} + + + + + + GitHub + +
+
); }; diff --git a/src/pages/user/login/__snapshots__/login.test.tsx.snap b/src/pages/user/login/__snapshots__/login.test.tsx.snap index dfe198a7..e799acfc 100644 --- a/src/pages/user/login/__snapshots__/login.test.tsx.snap +++ b/src/pages/user/login/__snapshots__/login.test.tsx.snap @@ -454,77 +454,110 @@ exports[`Login Page should login success 1`] = ` - + @@ -983,77 +1016,110 @@ exports[`Login Page should show login form 1`] = ` - + diff --git a/src/pages/user/login/login.test.tsx b/src/pages/user/login/login.test.tsx index 77c6e471..a2606e85 100644 --- a/src/pages/user/login/login.test.tsx +++ b/src/pages/user/login/login.test.tsx @@ -81,7 +81,7 @@ describe('Login Page', () => { await (await rootContainer.findByText('Login')).click(); // Wait for login to succeed and navigate to home page - await rootContainer.findByText('Ant Design Pro', undefined, { + await rootContainer.findByText(/Ant Design Pro/, undefined, { timeout: 10000, }); diff --git a/tests/setupTests.jsx b/tests/setupTests.jsx index 7f7a4a39..24c1d51a 100644 --- a/tests/setupTests.jsx +++ b/tests/setupTests.jsx @@ -1,56 +1,71 @@ -import { defaultConfig } from 'antd/lib/theme/internal'; +import { defaultConfig } from 'antd/lib/theme/internal'; defaultConfig.hashed = false; +const store = {}; + const localStorageMock = { - getItem: jest.fn(), - setItem: jest.fn(), - removeItem: jest.fn(), - clear: jest.fn(), + getItem: jest.fn((key) => store[key] ?? null), + setItem: jest.fn((key, value) => { + store[key] = String(value); + }), + removeItem: jest.fn((key) => { + delete store[key]; + }), + clear: jest.fn(() => { + Object.keys(store).forEach((key) => { + delete store[key]; + }); + }), }; -global.localStorage = localStorageMock; +globalThis.localStorage = localStorageMock; Object.defineProperty(URL, 'createObjectURL', { writable: true, value: jest.fn(), }); -class Worker { +class MockWorker { constructor(stringUrl) { this.url = stringUrl; - this.onmessage = () => {}; + this.onmessage = null; } postMessage(msg) { - this.onmessage(msg); + if (typeof this.onmessage === 'function') { + this.onmessage({ data: msg }); + } } + + terminate() {} + addEventListener() {} + removeEventListener() {} } -window.Worker = Worker; -// Polyfill MessageChannel for environments (like jest/jsdom) that don't provide it -if (typeof global.MessageChannel === 'undefined') { + +globalThis.Worker = MockWorker; + +if (typeof globalThis.MessageChannel === 'undefined') { class PolyMessageChannel { constructor() { const channel = this; + this.port1 = { + onmessage: null, postMessage(msg) { setTimeout(() => { - if ( - channel.port2 && - typeof channel.port2.onmessage === 'function' - ) { + if (typeof channel.port2.onmessage === 'function') { channel.port2.onmessage({ data: msg }); } }, 0); }, }; + this.port2 = { + onmessage: null, postMessage(msg) { setTimeout(() => { - if ( - channel.port1 && - typeof channel.port1.onmessage === 'function' - ) { + if (typeof channel.port1.onmessage === 'function') { channel.port1.onmessage({ data: msg }); } }, 0); @@ -59,39 +74,28 @@ if (typeof global.MessageChannel === 'undefined') { } } - global.MessageChannel = PolyMessageChannel; - if (typeof window !== 'undefined') { - window.MessageChannel = PolyMessageChannel; - } + globalThis.MessageChannel = PolyMessageChannel; } -if (typeof window !== 'undefined') { - // ref: https://github.com/ant-design/ant-design/issues/18774 - if (!window.matchMedia) { - Object.defineProperty(global.window, 'matchMedia', { - writable: true, - configurable: true, - value: jest.fn(() => ({ - matches: false, - addListener: jest.fn(), - removeListener: jest.fn(), - })), - }); - } - if (!window.matchMedia) { - Object.defineProperty(global.window, 'matchMedia', { - writable: true, - configurable: true, - value: jest.fn((query) => ({ - matches: query.includes('max-width'), - addListener: jest.fn(), - removeListener: jest.fn(), - })), - }); - } +if (typeof window !== 'undefined' && !window.matchMedia) { + Object.defineProperty(window, 'matchMedia', { + writable: true, + configurable: true, + value: jest.fn((query) => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), + }); } -const errorLog = console.error; -Object.defineProperty(global.window.console, 'error', { + +const originalError = console.error; +Object.defineProperty(window.console, 'error', { writable: true, configurable: true, value: (...rest) => { @@ -103,12 +107,11 @@ Object.defineProperty(global.window.console, 'error', { ) { return; } - errorLog(...rest); + originalError(...rest); }, }); -// Mock ResizeObserver -global.ResizeObserver = class ResizeObserver { +globalThis.ResizeObserver = class { observe() {} unobserve() {} disconnect() {}