feat(product-center): implement Figma-designed UI, upload module, and website tests
- Redesign website homepage & product-center per Figma (fonts, logos, hero/footer/customer cases) - Add API upload module (multer) with static serving for uploads/public assets - Add OriginGood.delisted flag and SDS request retry logic - Add admin ImageUpload component and goods import/upload flows - Add vitest suite for website components and composables (32 tests) - Add skills, docs, plans and PRODUCT.md
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
import { mount } from '@vue/test-utils';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { resolve } from 'node:path';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { ref } from 'vue';
|
||||
import FeatureCards from '../app/components/FeatureCards.vue';
|
||||
import StepProcess from '../app/components/StepProcess.vue';
|
||||
|
||||
const readComponent = (name: string) =>
|
||||
readFileSync(resolve(process.cwd(), `app/components/${name}.vue`), 'utf8');
|
||||
|
||||
describe('homepage navigation and carousel behavior', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('uses current-page login navigation for every homepage CTA', () => {
|
||||
const components = ['AppHeader', 'HeroBanner', 'StepProcess', 'CtaBanner'];
|
||||
|
||||
for (const component of components) {
|
||||
const source = readComponent(component);
|
||||
expect(source).not.toMatch(/target="_blank"[^>]*>\s*(登录|立即开始|免费试用|立即免费使用)/u);
|
||||
expect(source).toContain("window.location.href = 'https://inkpod.vip/user/login'");
|
||||
}
|
||||
});
|
||||
|
||||
it('advances the four-step content automatically', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal('ref', ref);
|
||||
const wrapper = mount(StepProcess);
|
||||
|
||||
expect(wrapper.get('[data-testid="step-image-0"]').classes()).toContain('opacity-100');
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(wrapper.get('[data-testid="step-image-1"]').classes()).toContain('opacity-100');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('fills the active step progress line before advancing', () => {
|
||||
const source = readComponent('StepProcess');
|
||||
expect(source).toContain('step-progress');
|
||||
expect(source).toContain('animation: fill-step-progress 2s linear forwards');
|
||||
expect(source).toContain('@keyframes fill-step-progress');
|
||||
expect(source).toContain('to { width: 100%; }');
|
||||
expect(source).toContain('class="step-title text-xl lg:text-4xl"');
|
||||
expect(source).not.toContain('step-title text-xl font-bold');
|
||||
});
|
||||
|
||||
it('advances the feature content every two seconds', async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.stubGlobal('ref', ref);
|
||||
const wrapper = mount(FeatureCards);
|
||||
|
||||
expect(wrapper.get('[data-testid="feature-image-0"]').classes()).toContain('opacity-100');
|
||||
await vi.advanceTimersByTimeAsync(2000);
|
||||
expect(wrapper.get('[data-testid="feature-image-1"]').classes()).toContain('opacity-100');
|
||||
wrapper.unmount();
|
||||
});
|
||||
|
||||
it('keeps feature items aligned with the image and uses restrained titles', () => {
|
||||
const source = readComponent('FeatureCards');
|
||||
expect(source).toContain('feature-list');
|
||||
expect(source).toContain('feature-item-title text-xl lg:text-3xl');
|
||||
expect(source).toContain('grid-template-rows: repeat(4, minmax(0, 1fr))');
|
||||
});
|
||||
|
||||
it('runs the customer-case track as an automatic marquee', () => {
|
||||
const source = readComponent('CustomerCases');
|
||||
expect(source).toContain('animation: case-marquee 30s');
|
||||
expect(source).toContain('@media (prefers-reduced-motion: reduce)');
|
||||
});
|
||||
|
||||
it('keeps the hero visual as a capped image instead of a background', () => {
|
||||
const source = readComponent('HeroBanner');
|
||||
expect(source).toMatch(/<img[^>]+src="\/主视觉\.png"/u);
|
||||
expect(source).not.toMatch(/background(?:-image)?:\s*[^;]*主视觉\.png/u);
|
||||
expect(source).toContain('hero-visual');
|
||||
});
|
||||
|
||||
it('styles the hero emphasis and separates the three selling points', () => {
|
||||
const source = readComponent('HeroBanner');
|
||||
expect(source).toContain('hero-copy');
|
||||
expect(source).toContain('hero-gradient-text');
|
||||
expect(source).toContain('linear-gradient(90deg, #111827 0%, #111827 28%, #ff6800 100%)');
|
||||
expect(source).toContain('.hero-copy { align-items: flex-start; }');
|
||||
expect(source).toContain('hero-subtitle font-thin');
|
||||
expect(source).toContain('selling-point-separator');
|
||||
expect(source).toContain('aria-hidden="true">|</span>');
|
||||
});
|
||||
|
||||
it('loads the supplied PingFang SC font weights globally', () => {
|
||||
const css = readFileSync(resolve(process.cwd(), 'app/assets/css/tailwind.css'), 'utf8');
|
||||
expect(css).toContain("font-family: 'PingFang SC'");
|
||||
expect(css).toContain("url('/fonts/pingfang-sc-regular.ttf')");
|
||||
expect(css).toContain("url('/fonts/pingfang-sc-thin.ttf')");
|
||||
expect(css).toContain("url('/fonts/pingfang-sc-medium.ttf')");
|
||||
expect(css).toContain("url('/fonts/pingfang-sc-semibold.ttf')");
|
||||
expect(existsSync(resolve(process.cwd(), 'public/fonts/pingfang-sc-regular.ttf'))).toBe(true);
|
||||
expect(existsSync(resolve(process.cwd(), 'public/fonts/pingfang-sc-thin.ttf'))).toBe(true);
|
||||
expect(existsSync(resolve(process.cwd(), 'public/fonts/pingfang-sc-medium.ttf'))).toBe(true);
|
||||
expect(existsSync(resolve(process.cwd(), 'public/fonts/pingfang-sc-semibold.ttf'))).toBe(true);
|
||||
});
|
||||
|
||||
it('uses the theme color for trust statistics', () => {
|
||||
const source = readComponent('TrustSection');
|
||||
expect(source).toContain('trust-stat-value text-primary');
|
||||
expect(source).toContain('trust-illustration');
|
||||
expect(source).toContain('margin-bottom: -40px');
|
||||
});
|
||||
|
||||
it('temporarily hides recommendation and solution navigation', () => {
|
||||
const source = readComponent('AppHeader');
|
||||
expect(source).toContain('v-if="SHOW_EXTENDED_NAV"');
|
||||
});
|
||||
|
||||
it('uses a compact desktop navigation font size', () => {
|
||||
const source = readComponent('AppHeader');
|
||||
expect(source).toContain('desktop-nav');
|
||||
expect(source).toContain('.desktop-nav { font-size: 18px; }');
|
||||
});
|
||||
|
||||
it('aligns country flags and keeps the more-products action at the bottom', () => {
|
||||
const source = readComponent('PodProducts');
|
||||
expect(source).toContain('country-button');
|
||||
expect(source).toContain('country-flag');
|
||||
expect(source).toContain('more-products');
|
||||
expect(source).toContain('more-products-link text-primary');
|
||||
expect(source).not.toContain('fa-arrow-right');
|
||||
expect(source).toContain('margin-top: auto');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user