diff --git a/babel.config.js b/babel.config.js new file mode 100644 index 0000000..58314ec --- /dev/null +++ b/babel.config.js @@ -0,0 +1,7 @@ +module.exports = { + presets: [ + ['@babel/preset-env', { targets: { node: 'current' } }], + ['@babel/preset-react', { runtime: 'automatic' }], + ['@babel/preset-typescript', { isTSX: true, allExtensions: true }], + ], +}; \ No newline at end of file diff --git a/e2e/package-lock.json b/e2e/package-lock.json index e163634..4152b46 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -13,6 +13,8 @@ }, "devDependencies": { "@axe-core/playwright": "^4.11.1", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@playwright/test": "^1.58.2", "@types/node": "^20.11.0", "allure-commandline": "^2.37.0", @@ -116,6 +118,19 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-compilation-targets": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", @@ -150,6 +165,38 @@ "semver": "bin/semver.js" } }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "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.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -159,6 +206,20 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -189,6 +250,61 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "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" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-string-parser": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", @@ -244,6 +360,185 @@ "node": ">=6.0.0" } }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/template": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", diff --git a/e2e/package.json b/e2e/package.json index fa0d49d..f6268fa 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -24,6 +24,8 @@ }, "devDependencies": { "@axe-core/playwright": "^4.11.1", + "@babel/preset-react": "^7.28.5", + "@babel/preset-typescript": "^7.28.5", "@playwright/test": "^1.58.2", "@types/node": "^20.11.0", "allure-commandline": "^2.37.0", diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/all-form-fields-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/all-form-fields-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3643401 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/all-form-fields-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-comparison-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-comparison-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3643401 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-comparison-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-desktop-view-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-desktop-view-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..98030c6 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-desktop-view-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-form-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-form-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3643401 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-form-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-form-filled-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-form-filled-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..740e26b Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-form-filled-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-full-page-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-full-page-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..de4a661 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-full-page-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-info-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-info-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..6867be9 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-info-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-info-card-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-info-card-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..6867be9 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-info-card-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-mobile-view-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-mobile-view-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..e58f936 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-mobile-view-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-no-animations-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-no-animations-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..de4a661 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-no-animations-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-page-header-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-page-header-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..2b5fb33 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-page-header-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-scrolled-page-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-scrolled-page-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ef56211 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-scrolled-page-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-tablet-view-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-tablet-view-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..fac270b Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/contact-tablet-view-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/email-input-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/email-input-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..529f82d Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/email-input-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/message-input-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/message-input-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..c0ec081 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/message-input-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..20d2b6d Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-focused-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-focused-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..352fccb Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-focused-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-hover-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-hover-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..20d2b6d Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/name-input-hover-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/subject-input-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/subject-input-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..1685a29 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/subject-input-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/submit-button-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/submit-button-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..2c9f500 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/submit-button-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/submit-button-hover-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/submit-button-hover-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..94f3a37 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/submit-button-hover-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/work-hours-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/work-hours-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..0795710 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/work-hours-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/work-hours-card-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/work-hours-card-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..0795710 Binary files /dev/null and b/e2e/src/tests/visual/contact-page.visual.spec.ts-snapshots/work-hours-card-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/about-section-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/about-section-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ba96055 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/about-section-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/all-sections-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/all-sections-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..a4e5f51 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/all-sections-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/cases-section-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/cases-section-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3cb09bb Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/cases-section-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/desktop-view-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/desktop-view-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..594b0d5 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/desktop-view-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/footer-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/footer-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..510388b Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/footer-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/full-page-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/full-page-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3031c63 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/full-page-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/header-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/header-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..dbe9fcd Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/header-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/header-scrolled-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/header-scrolled-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ca44b6b Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/header-scrolled-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-comparison-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-comparison-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..0e0fa5d Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-comparison-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-section-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-section-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..0e0fa5d Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-section-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-title-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-title-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..b993fd2 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/hero-title-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/logo-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/logo-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..393f245 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/logo-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/mobile-menu-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/mobile-menu-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..de55f2b Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/mobile-menu-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/mobile-view-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/mobile-view-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..d9fb41e Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/mobile-view-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/news-section-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/news-section-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ae416cf Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/news-section-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/no-animations-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/no-animations-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3031c63 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/no-animations-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/products-section-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/products-section-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..06e4c83 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/products-section-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/scrolled-page-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/scrolled-page-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ac5d90b Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/scrolled-page-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/services-section-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/services-section-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..df57d66 Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/services-section-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/tablet-view-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/tablet-view-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ed7a3dc Binary files /dev/null and b/e2e/src/tests/visual/home-page.visual.spec.ts-snapshots/tablet-view-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/404-page-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/404-page-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..5772274 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/404-page-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/500-page-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/500-page-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..5772274 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/500-page-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3643401 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-errors-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-errors-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..e89987b Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-errors-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-filled-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-filled-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..a2532f2 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-filled-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-success-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-success-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..c4525c5 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-success-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-validation-failed-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-validation-failed-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..e89987b Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-form-validation-failed-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-desktop-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-desktop-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..98030c6 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-desktop-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-full-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-full-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..de4a661 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-full-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-mobile-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-mobile-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..e58f936 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-mobile-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-tablet-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-tablet-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..1bdab96 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/contact-page-tablet-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-cases-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-cases-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3cb09bb Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-cases-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-desktop-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-desktop-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..594b0d5 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-desktop-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-footer-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-footer-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..510388b Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-footer-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-full-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-full-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3031c63 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-full-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-header-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-header-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..dbe9fcd Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-header-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-hero-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-hero-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..0e0fa5d Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-hero-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-mobile-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-mobile-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..d9fb41e Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-mobile-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-news-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-news-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ae416cf Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-news-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-products-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-products-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..06e4c83 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-products-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-bottom-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-bottom-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..d599bf2 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-bottom-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-middle-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-middle-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..e02ffb3 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-middle-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-top-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-top-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ea3dd5a Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-scroll-top-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-services-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-services-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..df57d66 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-services-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-tablet-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-tablet-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ed7a3dc Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/homepage-tablet-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/input-focus-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/input-focus-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..352fccb Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/input-focus-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/mobile-menu-open-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/mobile-menu-open-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..dc082d4 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/mobile-menu-open-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/page-loaded-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/page-loaded-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3031c63 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/page-loaded-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/page-loading-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/page-loading-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..ea3dd5a Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/page-loading-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/theme-dark-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/theme-dark-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3031c63 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/theme-dark-Mobile-Chrome-darwin.png differ diff --git a/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/theme-light-Mobile-Chrome-darwin.png b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/theme-light-Mobile-Chrome-darwin.png new file mode 100644 index 0000000..3031c63 Binary files /dev/null and b/e2e/src/tests/visual/visual-regression.spec.ts-snapshots/theme-light-Mobile-Chrome-darwin.png differ diff --git a/jest.setup.js b/jest.setup.js index 063214a..ac05fcf 100644 --- a/jest.setup.js +++ b/jest.setup.js @@ -1,4 +1,4 @@ -import '@testing-library/jest-dom'; +require('@testing-library/jest-dom'); jest.mock('next-auth', () => { return { diff --git a/public/images/149A1D2F-D9FD-49C7-B139-142C50C5FE8B_1_201_a.jpeg b/public/images/149A1D2F-D9FD-49C7-B139-142C50C5FE8B_1_201_a.jpeg new file mode 100644 index 0000000..65d2c52 Binary files /dev/null and b/public/images/149A1D2F-D9FD-49C7-B139-142C50C5FE8B_1_201_a.jpeg differ diff --git a/public/images/qrcode_for_gh_a297181ff548_258.jpg b/public/images/qrcode_for_gh_a297181ff548_258.jpg index d1f7654..a47b8ec 100644 Binary files a/public/images/qrcode_for_gh_a297181ff548_258.jpg and b/public/images/qrcode_for_gh_a297181ff548_258.jpg differ diff --git a/scripts/fix-login-issue.sh b/scripts/fix-login-issue.sh new file mode 100644 index 0000000..7c8d3c9 --- /dev/null +++ b/scripts/fix-login-issue.sh @@ -0,0 +1,64 @@ +#!/bin/bash + +echo "🔧 修复登录问题" +echo "================" +echo "" + +# 颜色定义 +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +# 停止当前服务器 +echo "1. 停止当前服务器..." +if lsof -ti:3000 > /dev/null 2>&1; then + lsof -ti:3000 | xargs kill -9 2>/dev/null + echo -e "${GREEN}✅ 服务器已停止${NC}" +else + echo -e "${YELLOW}⚠️ 没有运行的服务器${NC}" +fi + +# 清除缓存 +echo "" +echo "2. 清除缓存..." +rm -rf .next +echo -e "${GREEN}✅ 缓存已清除${NC}" + +# 重新构建 +echo "" +echo "3. 重新构建应用..." +npm run build + +if [ $? -eq 0 ]; then + echo -e "${GREEN}✅ 构建成功${NC}" + + # 启动服务器 + echo "" + echo "4. 启动生产服务器..." + npm run start & + + sleep 3 + + if lsof -ti:3000 > /dev/null 2>&1; then + echo -e "${GREEN}✅ 服务器已启动${NC}" + echo "" + echo "==================================" + echo -e "${GREEN}🎉 修复完成!${NC}" + echo "==================================" + echo "" + echo "📧 管理员邮箱: admin@novalon.cn" + echo "🔑 管理员密码: admin123456" + echo "🌐 登录地址: http://localhost:3000/admin/login" + echo "" + echo "💡 提示:" + echo " - 打开浏览器控制台查看登录调试信息" + echo " - 如果仍有问题,请检查控制台错误" + echo " - 建议使用Chrome或Firefox浏览器" + else + echo -e "${RED}❌ 服务器启动失败${NC}" + fi +else + echo -e "${RED}❌ 构建失败${NC}" + exit 1 +fi \ No newline at end of file diff --git a/src/app/(marketing)/page.tsx b/src/app/(marketing)/page.tsx index fe9334c..32889f9 100644 --- a/src/app/(marketing)/page.tsx +++ b/src/app/(marketing)/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { Suspense, useEffect } from 'react'; +import { Suspense, useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; import dynamic from 'next/dynamic'; import { HeroSection } from "@/components/sections/hero-section"; @@ -46,9 +46,35 @@ const NewsSection = dynamic( } ); +interface SiteConfig { + feature_services?: { enabled: boolean; items: string[] }; + feature_products?: { enabled: boolean; showPricing: boolean; featuredProducts: string[] }; + feature_news?: { enabled: boolean; displayCount: number; categories: string[]; sortOrder: 'asc' | 'desc' }; +} + function HomeContent() { const searchParams = useSearchParams(); - + const [config, setConfig] = useState({}); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchConfig = async () => { + try { + const res = await fetch('/api/config'); + const data = await res.json(); + if (data.success) { + setConfig(data.data); + } + } catch (error) { + console.error('获取配置失败:', error); + } finally { + setLoading(false); + } + }; + + fetchConfig(); + }, []); + useEffect(() => { const section = searchParams.get('section'); if (section) { @@ -63,15 +89,23 @@ function HomeContent() { } return undefined; }, [searchParams]); - + + if (loading) { + return ; + } + + const showServices = config.feature_services?.enabled !== false; + const showProducts = config.feature_products?.enabled !== false; + const showNews = config.feature_news?.enabled !== false; + return (
- - + {showServices && } + {showProducts && } - + {showNews && }
); } diff --git a/src/app/admin/users/page.tsx b/src/app/admin/users/page.tsx index 19adae2..4af9c0d 100644 --- a/src/app/admin/users/page.tsx +++ b/src/app/admin/users/page.tsx @@ -38,6 +38,7 @@ export default function UsersPage() { const [showEditModal, setShowEditModal] = useState(false); const [selectedUser, setSelectedUser] = useState(null); const [saving, setSaving] = useState(false); + const [deletingUserId, setDeletingUserId] = useState(null); const [formData, setFormData] = useState({ email: '', @@ -94,20 +95,32 @@ export default function UsersPage() { }; const handleDelete = async (userId: string) => { - if (!confirm('确定要删除此用户吗?')) { + if (deletingUserId) { + console.log('删除操作正在进行中,请勿重复点击'); + return; + } + + if (!confirm('确定要删除此用户吗?此操作不可恢复。')) { return; } try { + setDeletingUserId(userId); const res = await fetch(`/api/admin/users/${userId}`, { method: 'DELETE' }); if (res.ok) { await fetchUsers(); + } else { + const data = await res.json(); + alert(data.error || '删除失败'); } } catch (error) { console.error('删除用户失败:', error); + alert('删除失败,请稍后重试'); + } finally { + setDeletingUserId(null); } }; @@ -196,7 +209,8 @@ export default function UsersPage() { diff --git a/src/app/api/admin/config/route.test.ts b/src/app/api/admin/config/route.test.ts index ad363bd..4072490 100644 --- a/src/app/api/admin/config/route.test.ts +++ b/src/app/api/admin/config/route.test.ts @@ -9,18 +9,24 @@ jest.mock('@/lib/auth/permissions', () => ({ hasPermission: jest.fn(), })); -jest.mock('@/db', () => ({ - db: { - select: jest.fn().mockReturnValue({ - from: jest.fn().mockReturnValue({ - where: jest.fn().mockReturnValue({ - limit: jest.fn().mockResolvedValue([]), - orderBy: jest.fn().mockResolvedValue([]), - }), +jest.mock('@/lib/auth/check-permission', () => ({ + checkIsAdmin: jest.fn(), + getAdminUserId: jest.fn(), +})); + +jest.mock('@/db', () => { + const mockSelect = jest.fn().mockReturnValue({ + from: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ + limit: jest.fn().mockResolvedValue([]), + orderBy: jest.fn().mockResolvedValue([]), }), }), - insert: jest.fn().mockReturnValue({ - values: jest.fn().mockReturnValue({ + }); + + const mockUpdate = jest.fn().mockReturnValue({ + set: jest.fn().mockReturnValue({ + where: jest.fn().mockReturnValue({ returning: jest.fn().mockResolvedValue([{ id: 'test-id', key: 'test_key', @@ -29,8 +35,27 @@ jest.mock('@/db', () => ({ }]), }), }), - }, -})); + }); + + return { + db: { + select: mockSelect, + update: mockUpdate, + insert: jest.fn().mockReturnValue({ + values: jest.fn().mockReturnValue({ + returning: jest.fn().mockResolvedValue([{ + id: 'test-id', + key: 'test_key', + value: 'test_value', + category: 'general', + }]), + }), + }), + }, + }; +}); + +const { checkIsAdmin: mockCheckIsAdmin, getAdminUserId: mockGetAdminUserId } = require('@/lib/auth/check-permission'); describe('/api/admin/config', () => { beforeEach(() => { @@ -39,35 +64,29 @@ describe('/api/admin/config', () => { describe('GET', () => { it('should return 401 if not authenticated', async () => { + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + const request = new NextRequest('http://localhost/api/admin/config'); const response = await GET(request); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 403 if no permission', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'viewer' } }); - hasPermission.mockReturnValue(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); const request = new NextRequest('http://localhost/api/admin/config'); const response = await GET(request); const data = await response.json(); expect(response.status).toBe(403); - expect(data.error).toBe('无权限'); + expect(data.error).toBe('无权限执行此操作'); }); it('should return configs if authenticated and has permission', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'admin' } }); - hasPermission.mockReturnValue(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); const request = new NextRequest('http://localhost/api/admin/config'); const response = await GET(request); @@ -81,8 +100,8 @@ describe('/api/admin/config', () => { describe('POST', () => { it('should return 401 if not authenticated', async () => { - const { auth } = require('@/lib/auth'); - auth.mockResolvedValue(null); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const request = new NextRequest('http://localhost/api/admin/config', { method: 'POST', @@ -91,16 +110,13 @@ describe('/api/admin/config', () => { const response = await POST(request); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 400 if missing required fields', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'admin' } }); - hasPermission.mockReturnValue(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); + mockGetAdminUserId.mockResolvedValueOnce('1'); const request = new NextRequest('http://localhost/api/admin/config', { method: 'POST', @@ -116,26 +132,8 @@ describe('/api/admin/config', () => { describe('PUT', () => { it('should return 401 if not authenticated', async () => { - const { auth } = require('@/lib/auth'); - auth.mockResolvedValue(null); - - const request = new NextRequest('http://localhost/api/admin/config', { - method: 'PUT', - body: JSON.stringify({ configs: [] }), - }); - const response = await PUT(request); - const data = await response.json(); - - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); - }); - - it('should return 403 if no permission', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'viewer' } }); - hasPermission.mockReturnValue(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const request = new NextRequest('http://localhost/api/admin/config', { method: 'PUT', @@ -145,15 +143,27 @@ describe('/api/admin/config', () => { const data = await response.json(); expect(response.status).toBe(403); - expect(data.error).toBe('无权限'); + expect(data.error).toBe('无权限执行此操作'); + }); + + it('should return 403 if no permission', async () => { + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); + + const request = new NextRequest('http://localhost/api/admin/config', { + method: 'PUT', + body: JSON.stringify({ configs: [] }), + }); + const response = await PUT(request); + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 400 if configs is not an array', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'admin' } }); - hasPermission.mockReturnValue(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); + mockGetAdminUserId.mockResolvedValueOnce('1'); const request = new NextRequest('http://localhost/api/admin/config', { method: 'PUT', diff --git a/src/app/api/admin/content/[id]/route.test.ts b/src/app/api/admin/content/[id]/route.test.ts index 3ffa350..ca28f16 100644 --- a/src/app/api/admin/content/[id]/route.test.ts +++ b/src/app/api/admin/content/[id]/route.test.ts @@ -24,6 +24,11 @@ jest.mock('@/lib/auth/permissions', () => ({ hasPermission: jest.fn(), })); +jest.mock('@/lib/auth/check-permission', () => ({ + checkIsAdmin: jest.fn(), + getAdminUserId: jest.fn(), +})); + jest.mock('@/lib/audit', () => ({ createAuditLog: jest.fn().mockResolvedValue({}), })); @@ -31,6 +36,7 @@ jest.mock('@/lib/audit', () => ({ const { db } = require('@/db'); const { auth } = require('@/lib/auth'); const { hasPermission } = require('@/lib/auth/permissions'); +const { checkIsAdmin: mockCheckIsAdmin, getAdminUserId: mockGetAdminUserId } = require('@/lib/auth/check-permission'); describe('GET /api/admin/content/[id]', () => { beforeEach(() => { @@ -38,22 +44,7 @@ describe('GET /api/admin/content/[id]', () => { }); it('should return 401 if not authenticated', async () => { - auth.mockResolvedValue(null); - - const { GET } = require('./route'); - const request = new NextRequest('http://localhost/api/admin/content/123'); - const params = Promise.resolve({ id: '123' }); - - const response = await GET(request, { params }); - const data = await response.json(); - - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); - }); - - it('should return 403 if no permission', async () => { - auth.mockResolvedValue({ user: { role: 'viewer' } }); - hasPermission.mockReturnValue(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); const { GET } = require('./route'); const request = new NextRequest('http://localhost/api/admin/content/123'); @@ -63,12 +54,25 @@ describe('GET /api/admin/content/[id]', () => { const data = await response.json(); expect(response.status).toBe(403); - expect(data.error).toBe('无权限'); + expect(data.error).toBe('无权限执行此操作'); + }); + + it('should return 403 if no permission', async () => { + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + + const { GET } = require('./route'); + const request = new NextRequest('http://localhost/api/admin/content/123'); + const params = Promise.resolve({ id: '123' }); + + const response = await GET(request, { params }); + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 404 if content not found', async () => { - auth.mockResolvedValue({ user: { role: 'admin' } }); - hasPermission.mockReturnValue(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); db.limit.mockResolvedValue([]); const { GET } = require('./route'); @@ -89,8 +93,7 @@ describe('GET /api/admin/content/[id]', () => { status: 'published', }; - auth.mockResolvedValue({ user: { role: 'admin' } }); - hasPermission.mockReturnValue(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); db.limit.mockResolvedValue([mockContent]); db.orderBy.mockResolvedValue([]); @@ -112,7 +115,8 @@ describe('PUT /api/admin/content/[id]', () => { }); it('should return 401 if not authenticated', async () => { - auth.mockResolvedValue(null); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const { PUT } = require('./route'); const request = new NextRequest('http://localhost/api/admin/content/123', { @@ -124,12 +128,12 @@ describe('PUT /api/admin/content/[id]', () => { const response = await PUT(request, { params }); const data = await response.json(); - expect(response.status).toBe(401); + expect(response.status).toBe(403); }); it('should return 403 if no permission', async () => { - auth.mockResolvedValue({ user: { role: 'viewer' } }); - hasPermission.mockReturnValue(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const { PUT } = require('./route'); const request = new NextRequest('http://localhost/api/admin/content/123', { @@ -151,7 +155,8 @@ describe('DELETE /api/admin/content/[id]', () => { }); it('should return 401 if not authenticated', async () => { - auth.mockResolvedValue(null); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const { DELETE } = require('./route'); const request = new NextRequest('http://localhost/api/admin/content/123', { @@ -162,12 +167,12 @@ describe('DELETE /api/admin/content/[id]', () => { const response = await DELETE(request, { params }); const data = await response.json(); - expect(response.status).toBe(401); + expect(response.status).toBe(403); }); it('should return 403 if no permission', async () => { - auth.mockResolvedValue({ user: { role: 'editor' } }); - hasPermission.mockReturnValue(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const { DELETE } = require('./route'); const request = new NextRequest('http://localhost/api/admin/content/123', { diff --git a/src/app/api/admin/content/route.test.ts b/src/app/api/admin/content/route.test.ts index 6ae766d..d3c5be8 100644 --- a/src/app/api/admin/content/route.test.ts +++ b/src/app/api/admin/content/route.test.ts @@ -4,6 +4,8 @@ import '@testing-library/jest-dom'; const mockAuth = jest.fn(); const mockHasPermission = jest.fn(); +const mockCheckIsAdmin = jest.fn(); +const mockGetAdminUserId = jest.fn(); const mockDbSelect = jest.fn(); const mockDbInsert = jest.fn(); @@ -11,6 +13,11 @@ jest.mock('@/lib/auth', () => ({ auth: mockAuth, })); +jest.mock('@/lib/auth/check-permission', () => ({ + checkIsAdmin: mockCheckIsAdmin, + getAdminUserId: mockGetAdminUserId, +})); + jest.mock('@/lib/auth/permissions', () => ({ hasPermission: mockHasPermission, })); @@ -65,35 +72,29 @@ describe('/api/admin/content', () => { describe('GET', () => { it('should return 401 when not authenticated', async () => { - mockAuth.mockResolvedValueOnce(null); - - const request = new NextRequest('http://localhost/api/admin/content'); - const response = await GET(request); - const data = await response.json(); - - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); - }); - - it('should return 403 when user lacks permission', async () => { - mockAuth.mockResolvedValueOnce({ - user: { id: '1', role: 'viewer' }, - }); - mockHasPermission.mockReturnValueOnce(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); const request = new NextRequest('http://localhost/api/admin/content'); const response = await GET(request); const data = await response.json(); expect(response.status).toBe(403); - expect(data.error).toBe('无权限'); + expect(data.error).toBe('无权限执行此操作'); + }); + + it('should return 403 when user lacks permission', async () => { + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false, userId: '1' }); + + const request = new NextRequest('http://localhost/api/admin/content'); + const response = await GET(request); + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return content list when authorized', async () => { - mockAuth.mockResolvedValueOnce({ - user: { id: '1', role: 'admin' }, - }); - mockHasPermission.mockReturnValueOnce(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); mockDbSelect.mockResolvedValueOnce([]); mockDbSelect.mockResolvedValueOnce([{ count: 0 }]); @@ -109,7 +110,8 @@ describe('/api/admin/content', () => { describe('POST', () => { it('should return 401 when not authenticated', async () => { - mockAuth.mockResolvedValueOnce(null); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const request = new NextRequest('http://localhost/api/admin/content', { method: 'POST', @@ -118,15 +120,14 @@ describe('/api/admin/content', () => { const response = await POST(request); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 400 when missing required fields', async () => { - mockAuth.mockResolvedValueOnce({ - user: { id: '1', role: 'admin' }, - }); - mockHasPermission.mockReturnValueOnce(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); + mockGetAdminUserId.mockResolvedValueOnce('1'); + mockDbSelect.mockResolvedValueOnce([]); const request = new NextRequest('http://localhost/api/admin/content', { method: 'POST', diff --git a/src/app/api/admin/upload/route.test.ts b/src/app/api/admin/upload/route.test.ts index fee9c95..1ac92e9 100644 --- a/src/app/api/admin/upload/route.test.ts +++ b/src/app/api/admin/upload/route.test.ts @@ -9,6 +9,11 @@ jest.mock('@/lib/auth/permissions', () => ({ hasPermission: jest.fn(), })); +jest.mock('@/lib/auth/check-permission', () => ({ + checkIsAdmin: jest.fn(), + getAdminUserId: jest.fn(), +})); + jest.mock('@/lib/audit', () => ({ createAuditLog: jest.fn(), })); @@ -24,6 +29,8 @@ jest.mock('@/lib/upload', () => ({ deleteFile: jest.fn(), })); +const { checkIsAdmin: mockCheckIsAdmin, getAdminUserId: mockGetAdminUserId } = require('@/lib/auth/check-permission'); + describe('/api/admin/upload', () => { beforeEach(() => { jest.clearAllMocks(); @@ -31,6 +38,9 @@ describe('/api/admin/upload', () => { describe('POST', () => { it('should return 401 if not authenticated', async () => { + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); + const formData = new FormData(); formData.append('file', new File(['test'], 'test.jpg', { type: 'image/jpeg' })); @@ -41,16 +51,13 @@ describe('/api/admin/upload', () => { const response = await POST(request); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 403 if no permission', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'viewer' } }); - hasPermission.mockReturnValue(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const request = new NextRequest('http://localhost/api/admin/upload', { method: 'POST', @@ -59,15 +66,12 @@ describe('/api/admin/upload', () => { const data = await response.json(); expect(response.status).toBe(403); - expect(data.error).toBe('无权限'); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 400 if no file', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'admin', id: 'test-user' } }); - hasPermission.mockReturnValue(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); + mockGetAdminUserId.mockResolvedValueOnce('1'); const request = { formData: jest.fn().mockResolvedValue(new FormData()), @@ -82,8 +86,8 @@ describe('/api/admin/upload', () => { describe('DELETE', () => { it('should return 401 if not authenticated', async () => { - const { auth } = require('@/lib/auth'); - auth.mockResolvedValue(null); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + mockGetAdminUserId.mockResolvedValueOnce(null); const request = new NextRequest('http://localhost/api/admin/upload?url=test.jpg', { method: 'DELETE', @@ -91,8 +95,8 @@ describe('/api/admin/upload', () => { const response = await DELETE(request); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); }); }); diff --git a/src/app/api/admin/users/[id]/route.test.ts b/src/app/api/admin/users/[id]/route.test.ts index 36b70d8..1b314fd 100644 --- a/src/app/api/admin/users/[id]/route.test.ts +++ b/src/app/api/admin/users/[id]/route.test.ts @@ -9,6 +9,10 @@ jest.mock('@/lib/auth/permissions', () => ({ hasPermission: jest.fn(), })); +jest.mock('@/lib/auth/check-permission', () => ({ + checkIsAdmin: jest.fn(), +})); + jest.mock('@/db', () => ({ db: { select: jest.fn().mockReturnValue({ @@ -18,7 +22,7 @@ jest.mock('@/db', () => ({ id: 'test-user-id', email: 'test@example.com', name: 'Test User', - role: 'admin', + isAdmin: true, }]), }), }), @@ -40,6 +44,8 @@ jest.mock('@/db', () => ({ }, })); +const { checkIsAdmin: mockCheckIsAdmin } = require('@/lib/auth/check-permission'); + describe('/api/admin/users/[id]', () => { beforeEach(() => { jest.clearAllMocks(); @@ -47,35 +53,29 @@ describe('/api/admin/users/[id]', () => { describe('GET', () => { it('should return 401 if not authenticated', async () => { + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); + const request = new NextRequest('http://localhost/api/admin/users/test-id'); const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) }); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return 403 if no permission', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'viewer' } }); - hasPermission.mockReturnValue(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); const request = new NextRequest('http://localhost/api/admin/users/test-id'); const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) }); const data = await response.json(); expect(response.status).toBe(403); - expect(data.error).toBe('无权限'); + expect(data.error).toBe('无权限执行此操作'); }); it('should return user if authenticated and has permission', async () => { - const { auth } = require('@/lib/auth'); - const { hasPermission } = require('@/lib/auth/permissions'); - - auth.mockResolvedValue({ user: { role: 'admin' } }); - hasPermission.mockReturnValue(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); const request = new NextRequest('http://localhost/api/admin/users/test-id'); const response = await GET(request, { params: Promise.resolve({ id: 'test-id' }) }); @@ -88,8 +88,7 @@ describe('/api/admin/users/[id]', () => { describe('PUT', () => { it('should return 401 if not authenticated', async () => { - const { auth } = require('@/lib/auth'); - auth.mockResolvedValue(null); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); const request = new NextRequest('http://localhost/api/admin/users/test-id', { method: 'PUT', @@ -98,15 +97,14 @@ describe('/api/admin/users/[id]', () => { const response = await PUT(request, { params: Promise.resolve({ id: 'test-id' }) }); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); }); describe('DELETE', () => { it('should return 401 if not authenticated', async () => { - const { auth } = require('@/lib/auth'); - auth.mockResolvedValue(null); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); const request = new NextRequest('http://localhost/api/admin/users/test-id', { method: 'DELETE', @@ -114,8 +112,8 @@ describe('/api/admin/users/[id]', () => { const response = await DELETE(request, { params: Promise.resolve({ id: 'test-id' }) }); const data = await response.json(); - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); }); }); diff --git a/src/app/api/admin/users/[id]/route.ts b/src/app/api/admin/users/[id]/route.ts index d13f27f..8d17f1c 100644 --- a/src/app/api/admin/users/[id]/route.ts +++ b/src/app/api/admin/users/[id]/route.ts @@ -11,7 +11,7 @@ export async function GET( { params }: { params: Promise<{ id: string }> } ) { try { - const { isAdmin, userId } = await checkIsAdmin(); + const { isAdmin } = await checkIsAdmin(); if (!isAdmin) { return forbidden(); @@ -19,10 +19,6 @@ export async function GET( const { id } = await params; - if (id !== userId) { - return forbidden('只能查看自己的信息'); - } - const user = await db .select({ id: users.id, @@ -51,7 +47,7 @@ export async function PUT( { params }: { params: Promise<{ id: string }> } ) { try { - const { isAdmin, userId } = await checkIsAdmin(); + const { isAdmin } = await checkIsAdmin(); if (!isAdmin) { return forbidden(); @@ -59,10 +55,6 @@ export async function PUT( const { id } = await params; - if (id !== userId) { - return forbidden('只能修改自己的信息'); - } - const body = await request.json(); const { email, name, password } = body; @@ -110,7 +102,7 @@ export async function DELETE( { params }: { params: Promise<{ id: string }> } ) { try { - const { isAdmin, userId } = await checkIsAdmin(); + const { isAdmin } = await checkIsAdmin(); if (!isAdmin) { return forbidden(); @@ -118,10 +110,6 @@ export async function DELETE( const { id } = await params; - if (id !== userId) { - return forbidden('不能删除其他用户'); - } - await db .delete(users) .where(eq(users.id, id)); diff --git a/src/app/api/admin/users/route.test.ts b/src/app/api/admin/users/route.test.ts index aa900ad..c7d8a50 100644 --- a/src/app/api/admin/users/route.test.ts +++ b/src/app/api/admin/users/route.test.ts @@ -4,6 +4,7 @@ import '@testing-library/jest-dom'; const mockAuth = jest.fn(); const mockHasPermission = jest.fn(); +const mockCheckIsAdmin = jest.fn(); const mockDbSelect = jest.fn(); const mockDbInsert = jest.fn(); @@ -11,6 +12,10 @@ jest.mock('@/lib/auth', () => ({ auth: mockAuth, })); +jest.mock('@/lib/auth/check-permission', () => ({ + checkIsAdmin: mockCheckIsAdmin, +})); + jest.mock('@/lib/auth/permissions', () => ({ hasPermission: mockHasPermission, })); @@ -22,7 +27,7 @@ jest.mock('@/db', () => ({ where: () => ({ limit: mockDbSelect, }), - orderBy: mockDbSelect, + orderBy: () => mockDbSelect(), }), }), insert: () => ({ @@ -35,6 +40,7 @@ jest.mock('@/db', () => ({ jest.mock('drizzle-orm', () => ({ eq: jest.fn(), + desc: jest.fn(), })); jest.mock('nanoid', () => ({ @@ -49,7 +55,7 @@ jest.mock('@/db/schema', () => ({ users: {}, })); -import { GET, POST } from './route'; +import { GET } from './route'; describe('/api/admin/users', () => { beforeEach(() => { @@ -58,37 +64,31 @@ describe('/api/admin/users', () => { describe('GET', () => { it('should return 401 when not authenticated', async () => { - mockAuth.mockResolvedValueOnce(null); - - const request = new NextRequest('http://localhost/api/admin/users'); - const response = await GET(request); - const data = await response.json(); - - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); - }); - - it('should return 403 when user lacks permission', async () => { - mockAuth.mockResolvedValueOnce({ - user: { id: '1', role: 'viewer' }, - }); - mockHasPermission.mockReturnValueOnce(false); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false }); const request = new NextRequest('http://localhost/api/admin/users'); const response = await GET(request); const data = await response.json(); expect(response.status).toBe(403); - expect(data.error).toBe('无权限'); + expect(data.error).toBe('无权限执行此操作'); + }); + + it('should return 403 when user lacks permission', async () => { + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: false, userId: '1' }); + + const request = new NextRequest('http://localhost/api/admin/users'); + const response = await GET(request); + const data = await response.json(); + + expect(response.status).toBe(403); + expect(data.error).toBe('无权限执行此操作'); }); it('should return users list when authorized', async () => { - mockAuth.mockResolvedValueOnce({ - user: { id: '1', role: 'admin' }, - }); - mockHasPermission.mockReturnValueOnce(true); + mockCheckIsAdmin.mockResolvedValueOnce({ isAdmin: true, userId: '1' }); mockDbSelect.mockResolvedValueOnce([ - { id: '1', email: 'admin@example.com', name: 'Admin', role: 'admin' }, + { id: '1', email: 'admin@example.com', name: 'Admin', isAdmin: true }, ]); const request = new NextRequest('http://localhost/api/admin/users'); @@ -99,37 +99,4 @@ describe('/api/admin/users', () => { expect(data.users).toBeDefined(); }); }); - - describe('POST', () => { - it('should return 401 when not authenticated', async () => { - mockAuth.mockResolvedValueOnce(null); - - const request = new NextRequest('http://localhost/api/admin/users', { - method: 'POST', - body: JSON.stringify({ email: 'test@example.com', name: 'Test', password: 'password', role: 'viewer' }), - }); - const response = await POST(request); - const data = await response.json(); - - expect(response.status).toBe(401); - expect(data.error).toBe('未授权'); - }); - - it('should return 400 when missing required fields', async () => { - mockAuth.mockResolvedValueOnce({ - user: { id: '1', role: 'admin' }, - }); - mockHasPermission.mockReturnValueOnce(true); - - const request = new NextRequest('http://localhost/api/admin/users', { - method: 'POST', - body: JSON.stringify({ email: 'test@example.com' }), - }); - const response = await POST(request); - const data = await response.json(); - - expect(response.status).toBe(400); - expect(data.error).toBe('缺少必填字段'); - }); - }); }); diff --git a/src/app/api/config/route.ts b/src/app/api/config/route.ts new file mode 100644 index 0000000..799fc3f --- /dev/null +++ b/src/app/api/config/route.ts @@ -0,0 +1,25 @@ +import { NextResponse } from 'next/server'; +import { db } from '@/db'; +import { siteConfig } from '@/db/schema'; + +export async function GET() { + try { + const allConfigs = await db.select().from(siteConfig); + + const configMap = allConfigs.reduce((acc, config) => { + acc[config.key] = config.value; + return acc; + }, {} as Record); + + return NextResponse.json({ + success: true, + data: configMap + }); + } catch (error) { + console.error('获取配置失败:', error); + return NextResponse.json( + { success: false, error: '获取配置失败' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/components/layout/footer.test.tsx b/src/components/layout/footer.test.tsx index ad343c6..0be7e68 100644 --- a/src/components/layout/footer.test.tsx +++ b/src/components/layout/footer.test.tsx @@ -102,16 +102,40 @@ describe('Footer', () => { it('should render contact details', () => { render(