diff --git a/.claude/skills/impeccable b/.claude/skills/impeccable deleted file mode 120000 index 0ed5553..0000000 --- a/.claude/skills/impeccable +++ /dev/null @@ -1 +0,0 @@ -../../.agents/skills/impeccable \ No newline at end of file diff --git a/.gitignore b/.gitignore index 8ce9def..e7c80a7 100644 --- a/.gitignore +++ b/.gitignore @@ -266,6 +266,15 @@ performance-baseline.json test-data-*.json test-results-*.json +# Stryker mutation testing temp files +.stryker-tmp/ + +# Pi framework temp files +.pi/ + +# Agent config (local only) +.agents/settings.local.json + # Additional E2E test artifacts e2e/blob-report/ e2e/report/ diff --git a/Jenkinsfile b/Jenkinsfile index 4f0af1d..755b278 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -162,6 +162,86 @@ pipeline { } } + // ====== L3: E2E + 用户旅程测试 ====== + stage('🌐 E2E 测试') { + when { + branch 'main' + beforeAgent true + } + steps { + nodejs(nodeJSInstallationName: "${NODE_VERSION}") { + sh ''' + echo "🔨 构建用于 E2E 测试..." + npm run build + echo "🚀 启动预览服务器..." + npx serve dist -l 3000 & + sleep 5 + echo "🧪 运行 E2E 快速测试(@smoke + @critical)..." + cd e2e && npx playwright test --grep "@smoke|@critical" || echo "⚠️ E2E 测试部分失败,继续执行" + echo "🧪 运行用户旅程测试..." + npx playwright test --grep @journey || echo "⚠️ 用户旅程测试部分失败,继续执行" + ''' + } + } + post { + always { + sh 'kill $(lsof -t -i:3000) 2>/dev/null || true' + publishHTML(target: [ + allowMissing: true, + reportDir: 'e2e/playwright-report', + reportFiles: 'index.html', + reportName: 'E2E Test Report' + ]) + } + failure { + archiveArtifacts artifacts: 'e2e/test-results/**/*.png', allowEmptyArchive: true + } + } + } + + // ====== L4: 视觉回归测试 ====== + stage('👁️ 视觉回归测试') { + when { + branch 'main' + beforeAgent true + } + steps { + nodejs(nodeJSInstallationName: "${NODE_VERSION}") { + sh ''' + echo "🚀 启动预览服务器..." + npx serve dist -l 3000 & + sleep 5 + echo "🧪 运行视觉回归测试..." + cd e2e && npx playwright test visual-regression.spec.ts --project=visual-chromium-desktop || echo "⚠️ 视觉回归测试失败,请检查基线是否需要更新" + ''' + } + } + post { + always { + sh 'kill $(lsof -t -i:3000) 2>/dev/null || true' + archiveArtifacts artifacts: 'e2e/test-results/**/*.png', allowEmptyArchive: true + } + } + } + + // ====== L5: 安全扫描 ====== + stage('🔒 安全扫描') { + when { + branch 'main' + beforeAgent true + } + steps { + nodejs(nodeJSInstallationName: "${NODE_VERSION}") { + sh ''' + echo "🔒 运行依赖安全审计..." + npm audit --audit-level=high || echo "⚠️ 存在高危依赖漏洞,请检查" + echo "🔒 检查安全响应头..." + npm run test:security:headers -- --url https://novalon.cn || echo "⚠️ 安全头检查未通过,请检查 Nginx 配置" + ''' + } + } + } + stage('🏗️ 构建 dist') { steps { nodejs(nodeJSInstallationName: "${NODE_VERSION}") { diff --git a/config/test/jest.config.js b/config/test/jest.config.js index adedc09..0c2142b 100644 --- a/config/test/jest.config.js +++ b/config/test/jest.config.js @@ -1,6 +1,9 @@ +const path = require('path'); + module.exports = { preset: 'ts-jest', testEnvironment: 'jsdom', + rootDir: path.resolve(__dirname, '../..'), roots: ['/src'], testMatch: ['**/__tests__/**/*.test.{ts,tsx}', '**/*.test.{ts,tsx}'], // 仅收集实际可单元测试的目录的覆盖率,排除 pages/API routes/admin 等需集成/E2E 测试的代码 diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts index cf2a5fe..a2ee48f 100644 --- a/e2e/playwright.config.ts +++ b/e2e/playwright.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ reporter: [ ['html', { open: 'never' }], ['list', { printSteps: true }], + ['allure-playwright', { outputFolder: 'allure-results' }], ], snapshotDir: './visual-snapshots', snapshotPathTemplate: '{snapshotDir}/{projectName}/{testFilePath}/{arg}-{projectName}{ext}', diff --git a/jest.config.js b/jest.config.js deleted file mode 120000 index 7f013e1..0000000 --- a/jest.config.js +++ /dev/null @@ -1 +0,0 @@ -config/test/jest.config.js \ No newline at end of file diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..adedc09 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,111 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + roots: ['/src'], + testMatch: ['**/__tests__/**/*.test.{ts,tsx}', '**/*.test.{ts,tsx}'], + // 仅收集实际可单元测试的目录的覆盖率,排除 pages/API routes/admin 等需集成/E2E 测试的代码 + collectCoverageFrom: [ + 'src/components/ui/**/*.{ts,tsx}', + 'src/components/layout/**/*.{ts,tsx}', + 'src/components/seo/**/*.{ts,tsx}', + 'src/components/detail/**/*.{ts,tsx}', + 'src/components/sections/**/*.{ts,tsx}', + 'src/components/content/**/*.{ts,tsx}', + 'src/hooks/**/*.{ts,tsx}', + 'src/lib/**/*.ts', + '!src/**/*.d.ts', + '!src/**/*.stories.{ts,tsx}', + '!src/**/__tests__/**', + ], + // 覆盖率阈值采用渐进提升策略:基于 Phase 1-5 实测值设定目录级阈值,避免全局平均掩盖核心模块缺口 + // 实测基准 (Phase 5): global(~33% stmts/~32% branches), hooks(~61%/~41%), lib(~45%/~43%), + // ui(~15%/~21%), layout(~59%/~48%), detail(~32%/~29%), sections(~31%/~35%), content(~29%/~5%), + // lib/cms(~67%/~83%), lib/constants(~94%/~81%) + coverageThreshold: { + global: { + branches: 30, + functions: 25, + lines: 32, + statements: 30, + }, + './src/hooks/': { + branches: 38, + functions: 60, + lines: 60, + statements: 60, + }, + './src/lib/': { + branches: 40, + functions: 45, + lines: 42, + statements: 42, + }, + './src/components/ui/': { + branches: 18, + functions: 10, + lines: 15, + statements: 14, + }, + './src/components/layout/': { + branches: 45, + functions: 35, + lines: 60, + statements: 55, + }, + './src/components/detail/': { + branches: 25, + functions: 22, + lines: 30, + statements: 30, + }, + './src/components/sections/': { + branches: 30, + functions: 18, + lines: 30, + statements: 30, + }, + './src/components/content/': { + branches: 4, + functions: 4, + lines: 25, + statements: 25, + }, + './src/components/seo/': { + branches: 0, + functions: 18, + lines: 40, + statements: 40, + }, + './src/lib/cms/': { + branches: 80, + functions: 75, + lines: 60, + statements: 65, + }, + './src/lib/constants/': { + branches: 88, + functions: 45, + lines: 85, + statements: 85, + }, + }, + coverageProvider: 'v8', + coverageReporters: ['text', 'lcov', 'html', 'json'], + coverageDirectory: '/coverage', + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + transform: { + // ts-jest 需要明确指定 jsx: 'react-jsx',因为项目 tsconfig 使用 'preserve' + // 由后续 SWC/Babel 处理,但 Jest 环境下没有这些工具链 + '^.+\\.(ts|tsx)$': ['ts-jest', { tsconfig: { jsx: 'react-jsx' } }], + }, + transformIgnorePatterns: [ + 'node_modules/(?!(nanoid|next-auth|@auth|@aws-sdk)/)', + ], + setupFilesAfterEnv: ['/jest.setup.js'], + testTimeout: 10000, + verbose: true, + maxWorkers: '50%', +}; diff --git a/jest.setup.js b/jest.setup.js deleted file mode 120000 index 6eced6b..0000000 --- a/jest.setup.js +++ /dev/null @@ -1 +0,0 @@ -config/test/jest.setup.js \ No newline at end of file diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 0000000..384dd5f --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,203 @@ +require('@testing-library/jest-dom'); + +const { TextEncoder, TextDecoder } = require('util'); +global.TextEncoder = TextEncoder; +global.TextDecoder = TextDecoder; + +jest.mock('nanoid', () => ({ + nanoid: jest.fn(() => 'test-id-123'), +})); + +jest.mock('@/generated/prisma/client', () => ({ + PrismaClient: jest.fn().mockImplementation(() => ({ + contentItem: { + findMany: jest.fn().mockResolvedValue([]), + findFirst: jest.fn().mockResolvedValue(null), + findUnique: jest.fn().mockResolvedValue(null), + }, + contentModel: { + findMany: jest.fn().mockResolvedValue([]), + }, + contentZone: { + findMany: jest.fn().mockResolvedValue([]), + findUnique: jest.fn().mockResolvedValue(null), + }, + })), +})); + +jest.mock('@/lib/cms/data-server', () => ({ + getPublishedItems: jest.fn().mockResolvedValue([]), + getPublishedItemBySlug: jest.fn().mockResolvedValue(null), + getItemById: jest.fn().mockResolvedValue(null), + getPageZones: jest.fn().mockResolvedValue([]), + getZone: jest.fn().mockResolvedValue(null), + getContentModels: jest.fn().mockResolvedValue([]), + getCases: jest.fn().mockResolvedValue([]), + getNews: jest.fn().mockResolvedValue([]), + getServices: jest.fn().mockResolvedValue([]), + getProducts: jest.fn().mockResolvedValue([]), + getSolutions: jest.fn().mockResolvedValue([]), + getStats: jest.fn().mockResolvedValue([]), + getHeroBanners: jest.fn().mockResolvedValue([]), + getCaseBySlug: jest.fn().mockResolvedValue(null), + getNewsBySlug: jest.fn().mockResolvedValue(null), + getServiceBySlug: jest.fn().mockResolvedValue(null), + getProductBySlug: jest.fn().mockResolvedValue(null), + getSolutionBySlug: jest.fn().mockResolvedValue(null), + getHomePageZones: jest.fn().mockResolvedValue([]), + getAllPublishedSlugs: jest.fn().mockResolvedValue([]), +})); + +jest.mock('next/dynamic', () => ({ + __esModule: true, + default: (_importFn, _options) => { + const MockComponent = (_props) => null; + MockComponent.displayName = 'DynamicComponent'; + MockComponent.preload = () => Promise.resolve(); + return MockComponent; + }, +})); + +jest.mock('next/server', () => ({ + NextRequest: class MockNextRequest { + constructor(input, init = {}) { + this.url = typeof input === 'string' ? input : input.url; + this.method = init.method || 'GET'; + this.headers = new Headers(init.headers); + this.body = init.body; + } + + async json() { + return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; + } + }, + NextResponse: { + json: (body, init = {}) => ({ + status: init.status || 200, + json: async () => body, + }), + }, +})); + +global.console = { + ...console, + error: jest.fn(), + warn: jest.fn(), + log: jest.fn(), +}; + +class MockIntersectionObserver { + constructor(callback, options = {}) { + this.callback = callback; + this.options = options; + this.elements = new Set(); + this.observationEntries = []; + } + + observe(element) { + this.elements.add(element); + const entry = { + isIntersecting: true, + target: element, + boundingClientRect: element.getBoundingClientRect ? element.getBoundingClientRect() : {}, + intersectionRatio: 1, + intersectionRect: {}, + rootBounds: {}, + time: Date.now(), + }; + this.observationEntries.push(entry); + this.callback(this.observationEntries, this); + } + + unobserve(element) { + this.elements.delete(element); + this.observationEntries = this.observationEntries.filter( + entry => entry.target !== element + ); + } + + disconnect() { + this.elements.clear(); + this.observationEntries = []; + } + + takeRecords() { + return this.observationEntries; + } +} + +global.IntersectionObserver = MockIntersectionObserver; +global.IntersectionObserverEntry = class IntersectionObserverEntry { + constructor() { + this.isIntersecting = true; + this.target = {}; + this.boundingClientRect = {}; + this.intersectionRatio = 1; + this.intersectionRect = {}; + this.rootBounds = {}; + this.time = Date.now(); + } +}; + +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: jest.fn().mockImplementation(query => ({ + matches: false, + media: query, + onchange: null, + addListener: jest.fn(), + removeListener: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + dispatchEvent: jest.fn(), + })), +}); + +global.Request = class Request { + constructor(input, init = {}) { + this.url = typeof input === 'string' ? input : input.url; + this.method = init.method || 'GET'; + this.headers = new Headers(init.headers); + this.body = init.body; + } + + async json() { + return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; + } +}; + +global.Headers = class Headers { + constructor(init = {}) { + this.headers = {}; + if (init) { + Object.entries(init).forEach(([key, value]) => { + this.headers[key.toLowerCase()] = value; + }); + } + } + + get(name) { + return this.headers[name.toLowerCase()]; + } + + set(name, value) { + this.headers[name.toLowerCase()] = value; + } +}; + +global.Response = class Response { + constructor(body, init = {}) { + this.body = body; + this.status = init.status || 200; + this.statusText = init.statusText || 'OK'; + this.headers = new Headers(init.headers); + } + + async json() { + return typeof this.body === 'string' ? JSON.parse(this.body) : this.body; + } + + async text() { + return String(this.body); + } +}; diff --git a/package-lock.json b/package-lock.json index 3d915d3..764f4ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,6 +67,8 @@ "@commitlint/config-conventional": "^20.5.0", "@eslint/eslintrc": "^3.3.5", "@lhci/cli": "^0.15.1", + "@stryker-mutator/core": "^9.6.1", + "@stryker-mutator/jest-runner": "^9.6.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -78,6 +80,7 @@ "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", + "allure-playwright": "^3.10.2", "autoprefixer": "^10.4.20", "chrome-launcher": "^1.2.1", "eslint": "^9.25.1", @@ -576,13 +579,13 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.3" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -605,18 +608,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.3", - "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.3.tgz", - "integrity": "sha512-RpLYy2sb51oNLjuu1iD3bwBqCBWUzjO0ocp+iaCP/lJtb2CPLcnC2Fftw+4sAzaMELGeWTgExSKADbdo0GFVzA==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.29.0", + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "engines": { @@ -671,14 +674,14 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -715,22 +718,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", "dev": true, "license": "MIT", "engines": { @@ -756,15 +759,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -774,14 +777,14 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" }, "engines": { "node": ">=6.9.0" @@ -958,6 +961,24 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.7.tgz", + "integrity": "sha512-EtU0Hi3GvrTqD56xKmZvV/uCXK2ZbwVNPNLAquVItcAZpUhkXwWlo3Fmj0c2LxgSf2I8IDULeAepwNP1OefLXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/plugin-syntax-decorators": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-proposal-private-property-in-object": { "version": "7.21.0-placeholder-for-preset-env.2", "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", @@ -1026,6 +1047,22 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.29.7.tgz", + "integrity": "sha512-9MTTLbF39X6sqM92JPEsoI7++26hjZvzkxKZy64aMhWLH2mPkJ/Q3AV4QLmls3R14FpSpkOwQQfUh962JGQxxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/plugin-syntax-import-assertions": { "version": "7.28.6", "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", @@ -4149,6 +4186,394 @@ "url": "https://opencollective.com/libvips" } }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.1.tgz", + "integrity": "sha512-b6xmA/VlTe0ZgDQHDui+Nav470u7u49nRd8/iuhOcQPO9Ch7lGuogydhi2VOmNlZ+zXcM8IcPuNSwQcdJaF/kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core/node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@inquirer/core/node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.2.tgz", + "integrity": "sha512-ZRVd/oD+sYsUd5zVm0NflqEzlqfYCyHNsqkHl2oWXEUHs12tCbcSFi+wVFEvD8+LGRaMUsVrE7qeo6lSG/S1Vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/external-editor": "^3.0.3", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.1.tgz", + "integrity": "sha512-YmQpenjbFSHAK3sOd44puHh3V1KXXr+JiNpUztoSQ4drLh2rTVzTap/YtlAVu/5xavifIlBfNEzJ/neZJ1a/1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.3.tgz", + "integrity": "sha512-6thf5I8q7lZwzGLAxPaaGEREEkZ3nyePPDQ1oyobblxmEE8mqTLguScP7pDjUTAibiyb4hfXl+qjUEJ+di/aNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor/node_modules/chardet": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.2.0.tgz", + "integrity": "sha512-rddelWYNPRrXq6PtNEN2S3f6t9ILzvqaN5pVgi4kqt9jHQaXIial9PznB5iSPVlQSLNaaH22ItWz3EJtQ10+OA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@inquirer/external-editor/node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.2.tgz", + "integrity": "sha512-9K/DDBSQpOyZSkt6sOVP9Vo0TR7atX2kuILsUu0x3wVcVbe97lJwIJKMLdMw25tDYuXl/qp6erT0Xs1rfmcfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.1.tgz", + "integrity": "sha512-XF4IXAbPnGPgw0wsbC/i2tPcyfdZgDpUlhsqU0SfT4IRIGWha6Xm9VRgN5yYxJq+jnyXlfXI/nQ3ulfk0iEICA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.1.tgz", + "integrity": "sha512-3XBfF7DAsp5qeDsvN5Rd1HmbNokVvEQoUM0QLrRcybC9nX96w3Pbmu7qUsb3IT3J3jBvs2+mTXaKHOUsgHMLzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.2.tgz", + "integrity": "sha512-IYR/3C/paEVVQYQvdDlFZVjRCJVYHHON0XXMH91KO9GSxs0TdKYWlUdvfQl2EfAHDxUaN3IBffkE/BDTh5nJ6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.1", + "@inquirer/confirm": "^6.1.1", + "@inquirer/editor": "^5.2.2", + "@inquirer/expand": "^5.1.1", + "@inquirer/input": "^5.1.2", + "@inquirer/number": "^4.1.1", + "@inquirer/password": "^5.1.1", + "@inquirer/rawlist": "^5.3.1", + "@inquirer/search": "^4.2.1", + "@inquirer/select": "^5.2.1" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.1.tgz", + "integrity": "sha512-QqdTqQddL3qPX/PPrjobpsO25NZ4dWXgTLenrR445L2ptLEYE6Z+PD5c5CNDJNx4ugRgELAIpSIJxZaO2jJ2Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.1.tgz", + "integrity": "sha512-xJj8QWKRSrfKoBIITLZK61dD3zwo0Rz11fgDImku30/Oe81zMdIdGgrLY2h6RkJ+KZ/GhNYIRMKnH/62qBTA5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.1.tgz", + "integrity": "sha512-FlDndEUww8m7BfukO2nJa25vhD+H5jxxCv4oGioKqzyWz3nPHhhw4LKdYRSlXuAx7DsdWia7iyaBPKKS95Evfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/core": "^11.2.1", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, "node_modules/@istanbuljs/load-nyc-config": { "version": "1.1.0", "resolved": "https://registry.npmmirror.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", @@ -7568,6 +7993,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sentry-internal/tracing": { "version": "7.120.4", "resolved": "https://registry.npmmirror.com/@sentry-internal/tracing/-/tracing-7.120.4.tgz", @@ -8207,6 +8639,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmmirror.com/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -8320,6 +8765,374 @@ "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "license": "MIT" }, + "node_modules/@stryker-mutator/api": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/api/-/api-9.6.1.tgz", + "integrity": "sha512-g8VNoFWQWbx0pdal3Vt8jVCZW+v3sc3gi94iI0GVtVgUGTqphAjJF6EAruPTx0lqvtonsaAxn5TD36hcG1d6Wg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "tslib": "~2.8.0", + "typed-inject": "~5.0.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/core/-/core-9.6.1.tgz", + "integrity": "sha512-WMgnvf+Wyh/yiruhNZwc8w8DlzmmjXhPjSn5MR8RhAXzlnWji8TQrUYgBUkHk9bEgSaIlB3KZHm37iiU5Q2cLQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@inquirer/prompts": "^8.0.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/instrumenter": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "ajv": "~8.18.0", + "chalk": "~5.6.0", + "commander": "~14.0.0", + "diff-match-patch": "1.0.5", + "emoji-regex": "~10.6.0", + "execa": "~9.6.0", + "json-rpc-2.0": "^1.7.0", + "lodash.groupby": "~4.6.0", + "minimatch": "~10.2.4", + "mutation-server-protocol": "~0.4.0", + "mutation-testing-elements": "3.7.3", + "mutation-testing-metrics": "3.7.3", + "mutation-testing-report-schema": "3.7.3", + "npm-run-path": "~6.0.0", + "progress": "~2.0.3", + "rxjs": "~7.8.1", + "semver": "^7.6.3", + "source-map": "~0.7.4", + "tree-kill": "~1.2.2", + "tslib": "2.8.1", + "typed-inject": "~5.0.0", + "typed-rest-client": "~2.3.0" + }, + "bin": { + "stryker": "bin/stryker.js" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@stryker-mutator/core/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@stryker-mutator/core/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@stryker-mutator/core/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stryker-mutator/core/node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@stryker-mutator/core/node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stryker-mutator/core/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/core/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@stryker-mutator/core/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/core/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@stryker-mutator/core/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@stryker-mutator/instrumenter": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/instrumenter/-/instrumenter-9.6.1.tgz", + "integrity": "sha512-5K8wH4Pthly25c2uKKik4Dfcoeou7sbJdFS6u3QIYHlulgFVDJwtEMWTZGkZfs7IiUEXIDNa0keRACq5jn5AvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@babel/core": "~7.29.0", + "@babel/generator": "~7.29.0", + "@babel/parser": "~7.29.0", + "@babel/plugin-proposal-decorators": "~7.29.0", + "@babel/plugin-transform-explicit-resource-management": "^7.28.0", + "@babel/preset-typescript": "~7.28.0", + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "angular-html-parser": "~10.4.0", + "semver": "~7.7.0", + "tslib": "2.8.1", + "weapon-regex": "~1.3.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@stryker-mutator/instrumenter/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/jest-runner": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/jest-runner/-/jest-runner-9.6.1.tgz", + "integrity": "sha512-nIrIndfWwdweYkIcxJmyBTpl84nrXs9AE6A8vzEwwzUGvDb9kVvwBPfpEajtdAkjwPceH0pPuVrPkotvqcgYqQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@stryker-mutator/api": "9.6.1", + "@stryker-mutator/util": "9.6.1", + "semver": "~7.7.0", + "tslib": "~2.8.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@stryker-mutator/core": "9.6.1" + } + }, + "node_modules/@stryker-mutator/jest-runner/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@stryker-mutator/util": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@stryker-mutator/util/-/util-9.6.1.tgz", + "integrity": "sha512-Lk/ALVctJjFv1vvwR+CFoKzDCWvsBlq7flDUnmnpuwTrGbm156EdZD1Jjq4o8KdOap0ezUZqQNE9OAI1m2+pUQ==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -9999,6 +10812,47 @@ "ajv": "^8.8.2" } }, + "node_modules/allure-js-commons": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/allure-js-commons/-/allure-js-commons-3.10.2.tgz", + "integrity": "sha512-5nAjUF1iNPSQMwKtEsadclSta8C3L705/k/pIzGb9M6P9krgfRaCyaEHt8pkLlCP9NFj5xk3grjtnAhiLeT+Kg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "md5": "^2.3.0" + }, + "peerDependencies": { + "allure-playwright": "3.10.2" + }, + "peerDependenciesMeta": { + "allure-playwright": { + "optional": true + } + } + }, + "node_modules/allure-playwright": { + "version": "3.10.2", + "resolved": "https://registry.npmjs.org/allure-playwright/-/allure-playwright-3.10.2.tgz", + "integrity": "sha512-CGyhxSvx2bIkojhBy0pbQZ12vkLbOE3FBh39WgzPs+katwnuGPUE0whJhryG8aD/dlDALxMehL9G0k8WYBx4tQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "allure-js-commons": "3.10.2" + }, + "peerDependencies": { + "@playwright/test": ">=1.53.0" + } + }, + "node_modules/angular-html-parser": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/angular-html-parser/-/angular-html-parser-10.4.0.tgz", + "integrity": "sha512-++nLNyZwRfHqFh7akH5Gw/JYizoFlMRz0KRigfwfsLqV8ZqlcVRb1LkPEWdYvEKDnbktknM2J4BXaYUGrQZPww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmmirror.com/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -11051,6 +11905,16 @@ "dev": true, "license": "MIT" }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", @@ -11700,6 +12564,16 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -12014,6 +12888,17 @@ "node": ">=6" } }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, "node_modules/destr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", @@ -12070,6 +12955,13 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/diff-match-patch": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/diff-match-patch/-/diff-match-patch-1.0.5.tgz", + "integrity": "sha512-IayShXAgj/QMXgB0IWmKx+rOPuGMhqm5w6jvFxmVenXKIzRqTAAsbBPT3kWQeGANj3jGgvcvv4yK6SxqYmikgw==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz", @@ -13446,6 +14338,23 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, "node_modules/fast-uri": { "version": "3.1.4", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz", @@ -13462,6 +14371,16 @@ ], "license": "BSD-3-Clause" }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fastq": { "version": "1.20.1", "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.20.1.tgz", @@ -14735,6 +15654,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true, + "license": "MIT" + }, "node_modules/is-bun-module": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/is-bun-module/-/is-bun-module-2.0.0.tgz", @@ -15134,6 +16060,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-weakmap": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", @@ -16251,6 +17190,13 @@ "node": ">=12" } }, + "node_modules/js-md4": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/js-md4/-/js-md4-0.3.2.tgz", + "integrity": "sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==", + "dev": true, + "license": "MIT" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", @@ -16346,6 +17292,13 @@ "dev": true, "license": "MIT" }, + "node_modules/json-rpc-2.0": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/json-rpc-2.0/-/json-rpc-2.0-1.7.1.tgz", + "integrity": "sha512-JqZjhjAanbpkXIzFE7u8mE/iFblawwlXtONaCvRqI+pyABVz7B4M1EUNpyVW+dZjqgQ2L5HFmZCmOCgUKm00hg==", + "dev": true, + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -16655,18 +17608,6 @@ "node": "^20.19.0 || ^22.12.0 || >=23" } }, - "node_modules/lighthouse/node_modules/agent-base": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-9.0.0.tgz", - "integrity": "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20" - } - }, "node_modules/lighthouse/node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -16725,38 +17666,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/lighthouse/node_modules/data-uri-to-buffer": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-8.0.0.tgz", - "integrity": "sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20" - } - }, - "node_modules/lighthouse/node_modules/degenerator": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-7.0.1.tgz", - "integrity": "sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "ast-types": "^0.13.4", - "escodegen": "^2.1.0", - "esprima": "^4.0.1" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "quickjs-wasi": "^2.2.0" - } - }, "node_modules/lighthouse/node_modules/emoji-regex": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", @@ -16764,57 +17673,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lighthouse/node_modules/get-uri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-8.0.1.tgz", - "integrity": "sha512-/5N/P4Lrh0p/mDwlDRi7Y1+P2o/OyzZI3l6Iz1Ov6XXwwm1y3RlZLuo3gVgML99djrEDtV980bBxSuOeHLk8ww==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "basic-ftp": "^5.3.1", - "data-uri-to-buffer": "8.0.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/lighthouse/node_modules/http-proxy-agent": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-9.1.0.tgz", - "integrity": "sha512-2NxoveTT58mjYT4n3RPTEfCZGLMbidoO8XEieXfpSYxu+PQJ1qpx4ypwH6N+uF9twBPIvRRgvkvW5HUTYWENig==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "proxy-agent-negotiate": "1.1.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/lighthouse/node_modules/https-proxy-agent": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-9.1.0.tgz", - "integrity": "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "proxy-agent-negotiate": "1.1.0" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/lighthouse/node_modules/lighthouse-logger": { "version": "2.0.2", "resolved": "https://registry.npmmirror.com/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz", @@ -16826,18 +17684,6 @@ "marky": "^1.2.2" } }, - "node_modules/lighthouse/node_modules/lru-cache": { - "version": "7.18.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", - "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=12" - } - }, "node_modules/lighthouse/node_modules/open": { "version": "8.4.2", "resolved": "https://registry.npmmirror.com/open/-/open-8.4.2.tgz", @@ -16856,81 +17702,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lighthouse/node_modules/pac-proxy-agent": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-9.1.0.tgz", - "integrity": "sha512-1aU+1mpj3DrQPfo3gh+3Gap3G5x+axnMx1P/y0ZF2ch7kb2meyOCAH8K2k9d27ROsTE7TnAerzxqF9aon2jqnA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "get-uri": "8.0.1", - "http-proxy-agent": "9.1.0", - "https-proxy-agent": "9.1.0", - "pac-resolver": "9.0.1", - "quickjs-wasi": "^2.2.0", - "socks-proxy-agent": "10.1.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/lighthouse/node_modules/pac-resolver": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-9.0.1.tgz", - "integrity": "sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "degenerator": "7.0.1", - "netmask": "^2.0.2" - }, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "quickjs-wasi": "^2.2.0" - } - }, - "node_modules/lighthouse/node_modules/proxy-agent": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-8.0.2.tgz", - "integrity": "sha512-idLLRewuemWd7GH/BDJzGiB0dWGfT2SQs3jy6NtZtGWU9uPTTSdeC1/cdbqLwgzhfv027daGFuXX426e2Eg20A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "http-proxy-agent": "9.1.0", - "https-proxy-agent": "9.1.0", - "lru-cache": "^7.14.1", - "pac-proxy-agent": "9.1.0", - "proxy-from-env": "^2.0.0", - "socks-proxy-agent": "10.1.0" - }, - "engines": { - "node": ">= 20" - } - }, - "node_modules/lighthouse/node_modules/proxy-from-env": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", - "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=10" - } - }, "node_modules/lighthouse/node_modules/puppeteer-core": { "version": "25.3.0", "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.3.0.tgz", @@ -16973,23 +17744,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/lighthouse/node_modules/socks-proxy-agent": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-10.1.0.tgz", - "integrity": "sha512-WlMj/67cEJ6MDI1OcsnjuYKDNDoyPCCYZ249kuuXPiMDw9F8PXkVaQ7YWu3siTydfQ/4BEZcvGzu+aYvz7dDCQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "agent-base": "9.0.0", - "debug": "^4.3.4", - "socks": "^2.8.3" - }, - "engines": { - "node": ">= 20" - } - }, "node_modules/lighthouse/node_modules/string-width": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", @@ -17282,6 +18036,13 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.groupby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.groupby/-/lodash.groupby-4.6.0.tgz", + "integrity": "sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.includes": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", @@ -17645,6 +18406,18 @@ "node": ">= 0.4" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmmirror.com/media-typer/-/media-typer-0.3.0.tgz", @@ -17825,6 +18598,13 @@ "node": ">=4" } }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "license": "ISC" + }, "node_modules/minimatch": { "version": "3.1.5", "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", @@ -18006,6 +18786,43 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, + "node_modules/mutation-server-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/mutation-server-protocol/-/mutation-server-protocol-0.4.1.tgz", + "integrity": "sha512-SBGK0j8hLDne7bktgThKI8kGvGTx3rY3LAeQTmOKZ5bVnL/7TorLMvcVF7dIPJCu5RNUWhkkuF53kurygYVt3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "zod": "^4.1.12" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/mutation-testing-elements": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-elements/-/mutation-testing-elements-3.7.3.tgz", + "integrity": "sha512-SMeIPxngJpfjfNYctFpYQQtlBlZaVO0aoB3FKdwrI8Ee/2bkyUuCZzAOCLv1U9fnmfA37dPFq0Owduoxs2XgGQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/mutation-testing-metrics": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-metrics/-/mutation-testing-metrics-3.7.3.tgz", + "integrity": "sha512-B8QrP0ZomErzTPNlhrzKWPNBln+3afwBZPHv0Q7N8wZZTYxMptzb/Gdm3ExXVmioVYrtZAtsDs7W/T/b2AixOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mutation-testing-report-schema": "3.7.3" + } + }, + "node_modules/mutation-testing-report-schema": { + "version": "3.7.3", + "resolved": "https://registry.npmjs.org/mutation-testing-report-schema/-/mutation-testing-report-schema-3.7.3.tgz", + "integrity": "sha512-BHm3MYq+ckO+t5CtlG8zpqxc75rdJCkxVlE+fGuGJM3F7tNCQ/OW2N+TQVHN3BHsYa84+BFc6g3AwDYkUsw2MA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/mute-stream": { "version": "0.0.7", "resolved": "https://registry.npmmirror.com/mute-stream/-/mute-stream-0.0.7.tgz", @@ -18637,6 +19454,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", @@ -19111,6 +19941,22 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prisma": { "version": "6.19.3", "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", @@ -19337,26 +20183,6 @@ "node": ">= 14" } }, - "node_modules/proxy-agent-negotiate": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-agent-negotiate/-/proxy-agent-negotiate-1.1.0.tgz", - "integrity": "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">= 20" - }, - "peerDependencies": { - "kerberos": "^2.0.0" - }, - "peerDependenciesMeta": { - "kerberos": { - "optional": true - } - } - }, "node_modules/proxy-agent/node_modules/lru-cache": { "version": "7.18.3", "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-7.18.3.tgz", @@ -19475,15 +20301,6 @@ ], "license": "MIT" }, - "node_modules/quickjs-wasi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/quickjs-wasi/-/quickjs-wasi-2.2.0.tgz", - "integrity": "sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmmirror.com/range-parser/-/range-parser-1.2.1.tgz", @@ -21678,6 +22495,16 @@ "fsevents": "~2.3.3" } }, + "node_modules/tunnel": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", + "integrity": "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.11 <=0.7.0 || >=0.7.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", @@ -21806,6 +22633,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/typed-inject": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/typed-inject/-/typed-inject-5.0.0.tgz", + "integrity": "sha512-0Ql2ORqBORLMdAW89TQKZsb1PQkFGImFfVmncXWe7a+AA3+7dh7Se9exxZowH4kbnlvKEFkMxUYdHUpjYWFJaA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, "node_modules/typed-query-selector": { "version": "2.12.2", "resolved": "https://registry.npmmirror.com/typed-query-selector/-/typed-query-selector-2.12.2.tgz", @@ -21813,6 +22650,39 @@ "dev": true, "license": "MIT" }, + "node_modules/typed-rest-client": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/typed-rest-client/-/typed-rest-client-2.3.1.tgz", + "integrity": "sha512-k4kX5Up6qA68D0Cby2AK+6+vM5k3qTxe+/3FqhnHRExjY5cfbOnzjQZbP/LXleF8hVoDvDqxlgk9KK83HoBZlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "des.js": "^1.1.0", + "js-md4": "^0.3.2", + "qs": "6.15.1", + "tunnel": "0.0.6", + "underscore": "^1.13.8" + }, + "engines": { + "node": ">= 16.0.0" + } + }, + "node_modules/typed-rest-client/node_modules/qs": { + "version": "6.15.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz", + "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmmirror.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -21894,6 +22764,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "dev": true, + "license": "MIT" + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", @@ -21944,6 +22821,19 @@ "node": ">=4" } }, + "node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/unique-string": { "version": "2.0.0", "resolved": "https://registry.npmmirror.com/unique-string/-/unique-string-2.0.0.tgz", @@ -22192,6 +23082,13 @@ "node": ">=10.13.0" } }, + "node_modules/weapon-regex": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/weapon-regex/-/weapon-regex-1.3.6.tgz", + "integrity": "sha512-wsf1m1jmMrso5nhwVFJJHSubEBf3+pereGd7+nBKtYJ18KoB/PWJOHS3WRkwS04VrOU0iJr2bZU+l1QaTJ+9nA==", + "dev": true, + "license": "Apache-2.0" + }, "node_modules/web-features": { "version": "3.34.1", "resolved": "https://registry.npmjs.org/web-features/-/web-features-3.34.1.tgz", @@ -22689,6 +23586,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoctocolors": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.2.0.tgz", + "integrity": "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/zod": { "version": "4.4.3", "resolved": "https://registry.npmmirror.com/zod/-/zod-4.4.3.tgz", diff --git a/package.json b/package.json index bf11453..d72edef 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,6 @@ "test:coverage:check": "jest --coverage --ci", "coverage:report": "open coverage/lcov-report/index.html", "test:e2e": "cd e2e && npx playwright test --config=playwright.config.ts", - "test:smoke": "cd e2e && npx playwright test --grep @smoke", "test:visual": "cd e2e && npx playwright test visual-regression.spec.ts --project=visual-chromium-desktop", "test:visual:all": "cd e2e && npx playwright test visual-regression.spec.ts", "test:visual:update": "cd e2e && npx playwright test visual-regression.spec.ts --project=visual-chromium-desktop --update-snapshots", @@ -28,8 +27,19 @@ "test:visual:browsers": "cd e2e && npx playwright test visual-regression.spec.ts --project=visual-chromium-desktop --project=visual-firefox-desktop --project=visual-webkit-desktop", "test:performance": "k6 run tests/performance/load-test.js", "test:stress": "k6 run tests/performance/stress-test.js", + "test:smoke": "cd e2e && npx playwright test --grep @smoke", + "test:critical": "cd e2e && npx playwright test --grep @critical", + "test:e2e:fast": "cd e2e && npx playwright test --grep '@smoke|@critical'", + "test:e2e:standard": "cd e2e && npx playwright test --grep @regression", + "test:e2e:journey": "cd e2e && npx playwright test --grep @journey", + "test:e2e:mobile": "cd e2e && npx playwright test --grep @mobile", + "test:mutation": "rm -rf .stryker-tmp && npx stryker run --inPlace", + "test:mutation:quick": "rm -rf .stryker-tmp && npx stryker run --inPlace --mutate 'src/lib/utils.ts'", + "test:all": "npm run type-check && npm run lint && npm run test:coverage && npm run test:e2e:fast && npm run test:security:headers", "check:contrast": "tsx scripts/utils/check-color-contrast.ts", "check:headings": "tsx scripts/utils/check-heading-hierarchy.ts", + "test:security": "npm audit --audit-level=high && npm run test:security:headers", + "test:security:headers": "tsx scripts/utils/check-security-headers.ts", "lighthouse": "lhci autorun", "lighthouse:collect": "lhci collect", "lighthouse:assert": "lhci assert", @@ -98,6 +108,8 @@ "@commitlint/config-conventional": "^20.5.0", "@eslint/eslintrc": "^3.3.5", "@lhci/cli": "^0.15.1", + "@stryker-mutator/core": "^9.6.1", + "@stryker-mutator/jest-runner": "^9.6.1", "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", @@ -109,6 +121,7 @@ "@types/react-dom": "^18", "@typescript-eslint/eslint-plugin": "^8.65.0", "@typescript-eslint/parser": "^8.65.0", + "allure-playwright": "^3.10.2", "autoprefixer": "^10.4.20", "chrome-launcher": "^1.2.1", "eslint": "^9.25.1", diff --git a/playwright.config.ts b/playwright.config.ts deleted file mode 100644 index d623031..0000000 --- a/playwright.config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { defineConfig, devices } from '@playwright/test'; - -export default defineConfig({ - testDir: './e2e', - fullyParallel: true, - forbidOnly: !!process.env.CI, - retries: process.env.CI ? 2 : 0, - workers: process.env.CI ? 1 : undefined, - reporter: 'html', - use: { - baseURL: 'https://novalon.cn', - trace: 'on-first-retry', - screenshot: 'only-on-failure', - video: 'retain-on-failure', - }, - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, - }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, - }, - { - name: 'Mobile Chrome', - use: { ...devices['Pixel 5'] }, - }, - { - name: 'Mobile Safari', - use: { ...devices['iPhone 12'] }, - }, - ], -}); \ No newline at end of file diff --git a/scripts/utils/check-security-headers.ts b/scripts/utils/check-security-headers.ts new file mode 100644 index 0000000..abc18b9 --- /dev/null +++ b/scripts/utils/check-security-headers.ts @@ -0,0 +1,299 @@ +#!/usr/bin/env tsx + +/** + * 安全响应头检查脚本 + * + * 检查部署网站的安全头配置是否合规,包括: + * - 核心安全头(Content-Security-Policy, X-Content-Type-Options 等) + * - CORS 配置 + * - Cookie 安全标记 + */ + +interface SecurityCheck { + header: string; + expected: string; + actual: string | null; + status: 'pass' | 'fail' | 'warn'; + description: string; +} + +interface CookieCheck { + name: string; + hasHttpOnly: boolean; + hasSecure: boolean; + hasSameSite: boolean; + status: 'pass' | 'fail' | 'warn'; +} + +function parseArgs(): string { + const args = process.argv.slice(2); + const urlIndex = args.indexOf('--url'); + if (urlIndex !== -1 && args[urlIndex + 1]) { + return args[urlIndex + 1]!; + } + return 'https://novalon.cn'; +} + +function parseCookies(setCookieHeader: string | null): Array<{ name: string; flags: string[] }> { + if (!setCookieHeader) return []; + + const cookies: Array<{ name: string; flags: string[] }> = []; + + // Handle multiple Set-Cookie headers (joined by comma-newline) + const cookieStrings = setCookieHeader.split(/\n|,(?=\s*\w+=)/); + + for (const cookieStr of cookieStrings) { + const trimmed = cookieStr.trim(); + if (!trimmed) continue; + + const parts = trimmed.split(';').map((p) => p.trim()); + const nameValue = parts[0]!; + const name = nameValue.split('=')[0] || 'unknown'; + const flags = parts.slice(1).map((f) => f.toLowerCase()); + + cookies.push({ name, flags }); + } + + return cookies; +} + +function checkCookies(cookies: Array<{ name: string; flags: string[] }>): CookieCheck[] { + return cookies.map((cookie) => ({ + name: cookie.name, + hasHttpOnly: cookie.flags.some((f) => f === 'httponly'), + hasSecure: cookie.flags.some((f) => f === 'secure'), + hasSameSite: cookie.flags.some((f) => f.startsWith('samesite')), + status: 'pass' as const, + })); +} + +async function checkSecurityHeaders(url: string): Promise<{ + headerChecks: SecurityCheck[]; + cookieChecks: CookieCheck[]; +}> { + console.log(`\n🔒 安全响应头检查 — ${url}\n`); + + const response = await fetch(url, { + redirect: 'follow', + signal: AbortSignal.timeout(15000), + }); + + const headers = response.headers; + + const headerChecks: SecurityCheck[] = [ + { + header: 'Content-Security-Policy', + expected: '存在(需包含合理策略)', + actual: headers.get('content-security-policy'), + status: headers.get('content-security-policy') ? 'pass' : 'warn', + description: '防止 XSS 和数据注入攻击的核心策略', + }, + { + header: 'X-Content-Type-Options', + expected: 'nosniff', + actual: headers.get('x-content-type-options'), + status: headers.get('x-content-type-options') === 'nosniff' ? 'pass' : 'fail', + description: '防止 MIME 类型嗅探攻击', + }, + { + header: 'X-Frame-Options', + expected: 'DENY 或 SAMEORIGIN', + actual: headers.get('x-frame-options'), + status: (() => { + const val = headers.get('x-frame-options'); + if (val === 'DENY' || val === 'SAMEORIGIN') return 'pass' as const; + return val ? 'warn' as const : 'fail' as const; + })(), + description: '防止点击劫持(Clickjacking)攻击', + }, + { + header: 'Strict-Transport-Security', + expected: '存在(需包含 max-age)', + actual: headers.get('strict-transport-security'), + status: headers.get('strict-transport-security') ? 'pass' : 'fail', + description: '强制 HTTPS 连接,防止 SSL Strip 攻击', + }, + { + header: 'Referrer-Policy', + expected: '存在(如 strict-origin-when-cross-origin)', + actual: headers.get('referrer-policy'), + status: headers.get('referrer-policy') ? 'pass' : 'fail', + description: '控制 Referer 头信息的发送策略', + }, + { + header: 'Permissions-Policy', + expected: '存在(需配置合理权限)', + actual: headers.get('permissions-policy'), + status: headers.get('permissions-policy') ? 'pass' : 'warn', + description: '限制浏览器 API 权限(摄像头、麦克风等)', + }, + { + header: 'X-XSS-Protection', + expected: '存在(0 或 1; mode=block)', + actual: headers.get('x-xss-protection'), + status: (() => { + const val = headers.get('x-xss-protection'); + if (val === '0' || val === '1; mode=block') return 'pass' as const; + return val ? 'warn' as const : 'warn' as const; + })(), + description: '已废弃的 XSS 过滤器(现代浏览器不再需要)', + }, + ]; + + // CORS check + const corsOrigin = headers.get('access-control-allow-origin'); + if (corsOrigin) { + headerChecks.push({ + header: 'Access-Control-Allow-Origin', + expected: '非通配符(或仅对特定来源开放)', + actual: corsOrigin, + status: corsOrigin === '*' ? 'warn' : 'pass', + description: 'CORS 跨域配置', + }); + } else { + headerChecks.push({ + header: 'Access-Control-Allow-Origin', + expected: '无(不暴露 CORS 头)', + actual: null, + status: 'pass', + description: 'CORS 跨域配置(未设置,符合安全预期)', + }); + } + + // Cookie security check + const setCookie = headers.get('set-cookie'); + const parsedCookies = parseCookies(setCookie); + const cookieChecks = checkCookies(parsedCookies); + + return { headerChecks, cookieChecks }; +} + +function printTable(checks: SecurityCheck[]): void { + // Column widths + const headerWidth = 34; + const statusWidth = 6; + const expectedWidth = 38; + const actualWidth = 38; + + const separator = `├${'─'.repeat(headerWidth + 2)}┼${'─'.repeat(statusWidth + 2)}┼${'─'.repeat(expectedWidth + 2)}┼${'─'.repeat(actualWidth + 2)}┤`; + const topBorder = `┌${'─'.repeat(headerWidth + 2)}┬${'─'.repeat(statusWidth + 2)}┬${'─'.repeat(expectedWidth + 2)}┬${'─'.repeat(actualWidth + 2)}┐`; + const bottomBorder = `└${'─'.repeat(headerWidth + 2)}┴${'─'.repeat(statusWidth + 2)}┴${'─'.repeat(expectedWidth + 2)}┴${'─'.repeat(actualWidth + 2)}┘`; + + const headerRow = `│ ${'Header'.padEnd(headerWidth)} │ ${'Status'.padEnd(statusWidth)} │ ${'Expected'.padEnd(expectedWidth)} │ ${'Actual'.padEnd(actualWidth)} │`; + + console.log(topBorder); + console.log(headerRow); + console.log(separator); + + for (const check of checks) { + const statusIcon = check.status === 'pass' ? '✅ PASS' : check.status === 'fail' ? '❌ FAIL' : '⚠️ WARN'; + const actual = check.actual ?? '(未设置)'; + const row = `│ ${check.header.padEnd(headerWidth)} │ ${statusIcon.padEnd(statusWidth + 2)} │ ${check.expected.padEnd(expectedWidth)} │ ${actual.padEnd(actualWidth)} │`; + console.log(row); + } + + console.log(bottomBorder); +} + +function printCookieTable(cookieChecks: CookieCheck[]): void { + if (cookieChecks.length === 0) { + console.log('\n🍪 Cookie 安全标记: 无 Cookie 设置\n'); + return; + } + + const nameWidth = 24; + const httpOnlyWidth = 10; + const secureWidth = 8; + const sameSiteWidth = 10; + + const topBorder = `┌${'─'.repeat(nameWidth + 2)}┬${'─'.repeat(httpOnlyWidth + 2)}┬${'─'.repeat(secureWidth + 2)}┬${'─'.repeat(sameSiteWidth + 2)}┐`; + const separator = `├${'─'.repeat(nameWidth + 2)}┼${'─'.repeat(httpOnlyWidth + 2)}┼${'─'.repeat(secureWidth + 2)}┼${'─'.repeat(sameSiteWidth + 2)}┤`; + const bottomBorder = `└${'─'.repeat(nameWidth + 2)}┴${'─'.repeat(httpOnlyWidth + 2)}┴${'─'.repeat(secureWidth + 2)}┴${'─'.repeat(sameSiteWidth + 2)}┘`; + const headerRow = `│ ${'Cookie Name'.padEnd(nameWidth)} │ ${'HttpOnly'.padEnd(httpOnlyWidth)} │ ${'Secure'.padEnd(secureWidth)} │ ${'SameSite'.padEnd(sameSiteWidth)} │`; + + console.log('\n🍪 Cookie 安全标记\n'); + console.log(topBorder); + console.log(headerRow); + console.log(separator); + + for (const cookie of cookieChecks) { + const httpOnly = cookie.hasHttpOnly ? '✅' : '❌'; + const secure = cookie.hasSecure ? '✅' : '❌'; + const sameSite = cookie.hasSameSite ? '✅' : '❌'; + const row = `│ ${cookie.name.padEnd(nameWidth)} │ ${httpOnly.padEnd(httpOnlyWidth)} │ ${secure.padEnd(secureWidth)} │ ${sameSite.padEnd(sameSiteWidth)} │`; + console.log(row); + } + + console.log(bottomBorder); +} + +function printSummary(headerChecks: SecurityCheck[], cookieChecks: CookieCheck[]): void { + const total = headerChecks.length; + const passed = headerChecks.filter((c) => c.status === 'pass').length; + const failed = headerChecks.filter((c) => c.status === 'fail').length; + const warned = headerChecks.filter((c) => c.status === 'warn').length; + + const cookiePassed = cookieChecks.filter((c) => c.status === 'pass').length; + const cookieFailed = cookieChecks.filter((c) => c.status === 'fail').length; + + console.log('\n📊 检查摘要\n'); + console.log(` 安全头检查:`); + console.log(` 总计: ${total}`); + console.log(` ✅ 通过: ${passed}`); + console.log(` ⚠️ 警告: ${warned}`); + console.log(` ❌ 失败: ${failed}`); + + if (cookieChecks.length > 0) { + console.log(`\n Cookie 安全标记:`); + console.log(` 总计: ${cookieChecks.length}`); + console.log(` ✅ 通过: ${cookiePassed}`); + console.log(` ❌ 失败: ${cookieFailed}`); + } +} + +async function main() { + const url = parseArgs(); + + try { + const { headerChecks, cookieChecks } = await checkSecurityHeaders(url); + + // Print header check table + console.log('安全头检查结果:'); + printTable(headerChecks); + + // Print cookie check table + printCookieTable(cookieChecks); + + // Print descriptions for failed/warned checks + const issues = headerChecks.filter((c) => c.status !== 'pass'); + if (issues.length > 0) { + console.log('\n📝 说明:'); + for (const issue of issues) { + console.log(` • [${issue.status === 'fail' ? '❌' : '⚠️'}] ${issue.header}: ${issue.description}`); + if (issue.status === 'fail') { + console.log(` 期望: ${issue.expected}`); + console.log(` 实际: ${issue.actual ?? '(未设置)'}`); + } + } + } + + // Print summary + printSummary(headerChecks, cookieChecks); + + // Determine exit code + const hasFailures = headerChecks.some((c) => c.status === 'fail'); + if (hasFailures) { + console.log('\n❌ 存在失败的安全头检查项!'); + process.exit(1); + } else { + console.log('\n✅ 所有关键安全头检查通过!'); + process.exit(0); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + console.error(`\n❌ 检查执行失败: ${message}`); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/stryker.config.json b/stryker.config.json new file mode 100644 index 0000000..656d3d1 --- /dev/null +++ b/stryker.config.json @@ -0,0 +1,51 @@ +{ + "$schema": "./node_modules/@stryker-mutator/core/schema/stryker-schema.json", + "packageManager": "npm", + "reporters": ["html", "clear-text", "progress"], + "testRunner": "jest", + "coverageAnalysis": "perTest", + "mutate": [ + "src/lib/**/*.ts", + "src/hooks/**/*.{ts,tsx}", + "src/components/ui/**/*.{ts,tsx}", + "src/components/layout/**/*.{ts,tsx}", + "src/lib/cms/**/*.ts", + "!src/**/*.test.{ts,tsx}", + "!src/**/__tests__/**", + "!src/lib/constants/**" + ], + "thresholds": { + "high": 80, + "low": 60, + "break": 30 + }, + "timeoutMS": 60000, + "cleanTempDir": true, + "symlinkNodeModules": true, + "ignorePatterns": [ + ".eslintrc.json", + ".eslintrc", + "eslint.config.mjs", + ".lintstagedrc.js", + "commitlint.config.js", + "next.config.mjs", + "postcss.config.js", + "tailwind.config.js", + "tsconfig.json", + "public/", + "dist/", + ".next/", + "prisma/", + "scripts/", + "e2e/", + ".claude/", + ".trae/", + ".git/", + ".husky/", + "node_modules/", + "coverage/" + ], + "jest": { + "configFile": "config/test/jest.config.js" + } +} \ No newline at end of file