feat(e2e): 添加完整的E2E测试框架和测试用例
添加Playwright测试框架配置和基础页面对象 实现冒烟测试用例覆盖首页和联系页面核心功能 更新导航组件以支持滚动高亮功能 添加BackButton组件统一返回按钮行为 配置Woodpecker CI集成和测试报告生成
This commit is contained in:
@@ -0,0 +1,313 @@
|
||||
import { Page } from '@playwright/test';
|
||||
import { PerformanceMetrics, PerformanceThresholds } from '../types';
|
||||
|
||||
export class PerformanceMonitor {
|
||||
private page: Page;
|
||||
private metrics: PerformanceMetrics;
|
||||
private startTime: number;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.metrics = {
|
||||
loadTime: 0,
|
||||
firstContentfulPaint: 0,
|
||||
largestContentfulPaint: 0,
|
||||
timeToInteractive: 0,
|
||||
cumulativeLayoutShift: 0,
|
||||
firstInputDelay: 0,
|
||||
};
|
||||
this.startTime = 0;
|
||||
}
|
||||
|
||||
async startMonitoring(): Promise<void> {
|
||||
this.startTime = Date.now();
|
||||
|
||||
await this.page.evaluate(() => {
|
||||
window.performance.clearResourceTimings();
|
||||
});
|
||||
|
||||
await this.page.evaluate(() => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
entries.forEach((entry) => {
|
||||
if (entry.entryType === 'layout-shift' && !(entry as any).hadRecentInput) {
|
||||
(window as any).cumulativeLayoutShift = ((window as any).cumulativeLayoutShift || 0) + (entry as any).value;
|
||||
}
|
||||
});
|
||||
});
|
||||
observer.observe({ entryTypes: ['layout-shift'] });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async collectMetrics(): Promise<PerformanceMetrics> {
|
||||
const navigationTiming = await this.page.evaluate(() => {
|
||||
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
return {
|
||||
loadTime: timing.loadEventEnd - timing.fetchStart,
|
||||
domContentLoaded: timing.domContentLoadedEventEnd - timing.fetchStart,
|
||||
firstPaint: timing.responseStart - timing.fetchStart,
|
||||
};
|
||||
});
|
||||
|
||||
const paintTiming = await this.page.evaluate(() => {
|
||||
const paints = performance.getEntriesByType('paint');
|
||||
const fcp = paints.find((p) => p.name === 'first-contentful-paint');
|
||||
return {
|
||||
firstContentfulPaint: fcp ? fcp.startTime : 0,
|
||||
};
|
||||
});
|
||||
|
||||
const lcp = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
resolve(lastEntry ? lastEntry.startTime : 0);
|
||||
});
|
||||
observer.observe({ entryTypes: ['largest-contentful-paint'] });
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const cls = await this.page.evaluate(() => {
|
||||
return (window as any).cumulativeLayoutShift || 0;
|
||||
});
|
||||
|
||||
const tti = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const longTasks = entries.filter((e) => e.duration > 50);
|
||||
if (longTasks.length > 0) {
|
||||
resolve(longTasks[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['longtask'] });
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const fid = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
resolve(entries[0].processingStart - entries[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['first-input'] });
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
this.metrics = {
|
||||
loadTime: navigationTiming.loadTime,
|
||||
firstContentfulPaint: paintTiming.firstContentfulPaint,
|
||||
largestContentfulPaint: lcp,
|
||||
timeToInteractive: tti,
|
||||
cumulativeLayoutShift: cls,
|
||||
firstInputDelay: fid,
|
||||
};
|
||||
|
||||
return this.metrics;
|
||||
}
|
||||
|
||||
async measurePageLoad(): Promise<number> {
|
||||
const startTime = Date.now();
|
||||
await this.page.waitForLoadState('networkidle');
|
||||
const endTime = Date.now();
|
||||
return endTime - startTime;
|
||||
}
|
||||
|
||||
async measureFirstContentfulPaint(): Promise<number> {
|
||||
const fcp = await this.page.evaluate(() => {
|
||||
const paints = performance.getEntriesByType('paint');
|
||||
const fcpEntry = paints.find((p) => p.name === 'first-contentful-paint');
|
||||
return fcpEntry ? fcpEntry.startTime : 0;
|
||||
});
|
||||
return fcp;
|
||||
}
|
||||
|
||||
async measureLargestContentfulPaint(): Promise<number> {
|
||||
const lcp = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const lastEntry = entries[entries.length - 1];
|
||||
resolve(lastEntry ? lastEntry.startTime : 0);
|
||||
});
|
||||
observer.observe({ entryTypes: ['largest-contentful-paint'] });
|
||||
setTimeout(() => resolve(0), 5000);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return lcp;
|
||||
}
|
||||
|
||||
async measureCumulativeLayoutShift(): Promise<number> {
|
||||
const cls = await this.page.evaluate(() => {
|
||||
return (window as any).cumulativeLayoutShift || 0;
|
||||
});
|
||||
return cls;
|
||||
}
|
||||
|
||||
async measureTimeToInteractive(): Promise<number> {
|
||||
const tti = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
const longTasks = entries.filter((e) => e.duration > 50);
|
||||
if (longTasks.length > 0) {
|
||||
resolve(longTasks[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['longtask'] });
|
||||
setTimeout(() => resolve(0), 10000);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return tti;
|
||||
}
|
||||
|
||||
async measureFirstInputDelay(): Promise<number> {
|
||||
const fid = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
if ('PerformanceObserver' in window) {
|
||||
const observer = new PerformanceObserver((list) => {
|
||||
const entries = list.getEntries();
|
||||
if (entries.length > 0) {
|
||||
resolve(entries[0].processingStart - entries[0].startTime);
|
||||
}
|
||||
});
|
||||
observer.observe({ entryTypes: ['first-input'] });
|
||||
setTimeout(() => resolve(0), 5000);
|
||||
} else {
|
||||
resolve(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
return fid;
|
||||
}
|
||||
|
||||
async measureResourceTiming(): Promise<any[]> {
|
||||
const resources = await this.page.evaluate(() => {
|
||||
return performance.getEntriesByType('resource').map((r) => ({
|
||||
name: r.name,
|
||||
duration: r.duration,
|
||||
size: (r as any).transferSize,
|
||||
type: r.initiatorType,
|
||||
}));
|
||||
});
|
||||
return resources;
|
||||
}
|
||||
|
||||
async measureMemoryUsage(): Promise<number> {
|
||||
const memory = await this.page.evaluate(() => {
|
||||
return (performance as any).memory?.usedJSHeapSize || 0;
|
||||
});
|
||||
return memory;
|
||||
}
|
||||
|
||||
async measureFrameRate(): Promise<number> {
|
||||
const frameRate = await this.page.evaluate(() => {
|
||||
return new Promise<number>((resolve) => {
|
||||
let frames = 0;
|
||||
const startTime = performance.now();
|
||||
|
||||
function countFrames() {
|
||||
frames++;
|
||||
if (performance.now() - startTime >= 1000) {
|
||||
resolve(frames);
|
||||
} else {
|
||||
requestAnimationFrame(countFrames);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(countFrames);
|
||||
});
|
||||
});
|
||||
return frameRate;
|
||||
}
|
||||
|
||||
async measureDomContentLoaded(): Promise<number> {
|
||||
const dcl = await this.page.evaluate(() => {
|
||||
const timing = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
|
||||
return timing.domContentLoadedEventEnd - timing.fetchStart;
|
||||
});
|
||||
return dcl;
|
||||
}
|
||||
|
||||
validateMetrics(thresholds: PerformanceThresholds): { passed: boolean; violations: string[] } {
|
||||
const violations: string[] = [];
|
||||
|
||||
if (this.metrics.loadTime > thresholds.loadTime) {
|
||||
violations.push(`页面加载时间 ${this.metrics.loadTime}ms 超过阈值 ${thresholds.loadTime}ms`);
|
||||
}
|
||||
if (this.metrics.firstContentfulPaint > thresholds.firstContentfulPaint) {
|
||||
violations.push(`首次内容绘制 ${this.metrics.firstContentfulPaint}ms 超过阈值 ${thresholds.firstContentfulPaint}ms`);
|
||||
}
|
||||
if (this.metrics.largestContentfulPaint > thresholds.largestContentfulPaint) {
|
||||
violations.push(`最大内容绘制 ${this.metrics.largestContentfulPaint}ms 超过阈值 ${thresholds.largestContentfulPaint}ms`);
|
||||
}
|
||||
if (this.metrics.timeToInteractive > thresholds.timeToInteractive) {
|
||||
violations.push(`可交互时间 ${this.metrics.timeToInteractive}ms 超过阈值 ${thresholds.timeToInteractive}ms`);
|
||||
}
|
||||
if (this.metrics.cumulativeLayoutShift > thresholds.cumulativeLayoutShift) {
|
||||
violations.push(`累积布局偏移 ${this.metrics.cumulativeLayoutShift} 超过阈值 ${thresholds.cumulativeLayoutShift}`);
|
||||
}
|
||||
if (this.metrics.firstInputDelay > thresholds.firstInputDelay) {
|
||||
violations.push(`首次输入延迟 ${this.metrics.firstInputDelay}ms 超过阈值 ${thresholds.firstInputDelay}ms`);
|
||||
}
|
||||
|
||||
return {
|
||||
passed: violations.length === 0,
|
||||
violations,
|
||||
};
|
||||
}
|
||||
|
||||
getMetrics(): PerformanceMetrics {
|
||||
return this.metrics;
|
||||
}
|
||||
|
||||
async generateReport(): Promise<string> {
|
||||
const metrics = await this.collectMetrics();
|
||||
const resources = await this.measureResourceTiming();
|
||||
|
||||
let report = '=== 性能测试报告 ===\n\n';
|
||||
report += '核心指标:\n';
|
||||
report += `- 页面加载时间: ${metrics.loadTime.toFixed(2)}ms\n`;
|
||||
report += `- 首次内容绘制: ${metrics.firstContentfulPaint.toFixed(2)}ms\n`;
|
||||
report += `- 最大内容绘制: ${metrics.largestContentfulPaint.toFixed(2)}ms\n`;
|
||||
report += `- 可交互时间: ${metrics.timeToInteractive.toFixed(2)}ms\n`;
|
||||
report += `- 累积布局偏移: ${metrics.cumulativeLayoutShift.toFixed(4)}\n`;
|
||||
report += `- 首次输入延迟: ${metrics.firstInputDelay.toFixed(2)}ms\n\n`;
|
||||
|
||||
report += '资源加载:\n';
|
||||
const totalResources = resources.length;
|
||||
const totalSize = resources.reduce((sum, r) => sum + (r.size || 0), 0);
|
||||
const avgDuration = resources.reduce((sum, r) => sum + r.duration, 0) / totalResources;
|
||||
|
||||
report += `- 总资源数: ${totalResources}\n`;
|
||||
report += `- 总大小: ${(totalSize / 1024).toFixed(2)}KB\n`;
|
||||
report += `- 平均加载时间: ${avgDuration.toFixed(2)}ms\n`;
|
||||
|
||||
return report;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import { ContactFormData, TestData } from '../types';
|
||||
|
||||
export class TestDataGenerator {
|
||||
private static readonly FIRST_NAMES = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄', '周', '吴'];
|
||||
private static readonly LAST_NAMES = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '军'];
|
||||
private static readonly COMPANIES = ['科技有限公司', '信息技术有限公司', '网络技术有限公司', '数据科技有限公司', '智能科技有限公司'];
|
||||
private static readonly SUBJECTS = ['产品咨询', '技术支持', '商务合作', '其他', '意见反馈'];
|
||||
|
||||
static generateName(): string {
|
||||
const first = this.FIRST_NAMES[Math.floor(Math.random() * this.FIRST_NAMES.length)];
|
||||
const last = this.LAST_NAMES[Math.floor(Math.random() * this.LAST_NAMES.length)];
|
||||
return `${first}${last}`;
|
||||
}
|
||||
|
||||
static generateEmail(name?: string): string {
|
||||
const username = name || this.generateName();
|
||||
const domains = ['example.com', 'test.com', 'demo.com'];
|
||||
const domain = domains[Math.floor(Math.random() * domains.length)];
|
||||
return `${username}@${domain}`;
|
||||
}
|
||||
|
||||
static generatePhone(): string {
|
||||
const prefix = ['138', '139', '136', '137', '158', '159'][Math.floor(Math.random() * 6)];
|
||||
const middle = Math.floor(Math.random() * 9000 + 1000);
|
||||
const suffix = Math.floor(Math.random() * 9000 + 1000);
|
||||
return `${prefix}${middle}${suffix}`;
|
||||
}
|
||||
|
||||
static generateCompany(): string {
|
||||
const prefix = ['创新', '未来', '智慧', '科技', '数字'][Math.floor(Math.random() * 5)];
|
||||
const suffix = this.COMPANIES[Math.floor(Math.random() * this.COMPANIES.length)];
|
||||
return `${prefix}${suffix}`;
|
||||
}
|
||||
|
||||
static generateMessage(minLength: number = 10, maxLength: number = 100): string {
|
||||
const messages = [
|
||||
'您好,我对贵公司的产品很感兴趣,希望能了解更多信息。',
|
||||
'请问贵公司是否有相关的技术支持服务?',
|
||||
'我们正在寻找合作伙伴,希望能与贵公司建立联系。',
|
||||
'贵公司的产品功能很强大,希望能安排一次演示。',
|
||||
'我对贵公司的服务有些建议和想法,希望能与您交流。',
|
||||
'请问贵公司的产品价格如何?是否有优惠政策?',
|
||||
'我们公司正在评估相关技术方案,希望能了解贵公司的解决方案。',
|
||||
'您好,我想咨询一下贵公司的产品定制服务。',
|
||||
];
|
||||
return messages[Math.floor(Math.random() * messages.length)];
|
||||
}
|
||||
|
||||
static generateSubject(): string {
|
||||
return this.SUBJECTS[Math.floor(Math.random() * this.SUBJECTS.length)];
|
||||
}
|
||||
|
||||
static generateContactFormData(): ContactFormData {
|
||||
return {
|
||||
name: this.generateName(),
|
||||
email: this.generateEmail(),
|
||||
phone: this.generatePhone(),
|
||||
company: this.generateCompany(),
|
||||
message: this.generateMessage(),
|
||||
subject: this.generateSubject(),
|
||||
};
|
||||
}
|
||||
|
||||
static generateTestData(): TestData {
|
||||
return {
|
||||
name: this.generateName(),
|
||||
email: this.generateEmail(),
|
||||
phone: this.generatePhone(),
|
||||
company: this.generateCompany(),
|
||||
message: this.generateMessage(),
|
||||
};
|
||||
}
|
||||
|
||||
static generateInvalidEmail(): string {
|
||||
const invalidEmails = [
|
||||
'invalid-email',
|
||||
'@example.com',
|
||||
'user@',
|
||||
'user@domain',
|
||||
'user domain.com',
|
||||
];
|
||||
return invalidEmails[Math.floor(Math.random() * invalidEmails.length)];
|
||||
}
|
||||
|
||||
static generateInvalidPhone(): string {
|
||||
const invalidPhones = [
|
||||
'123',
|
||||
'1234567890123',
|
||||
'abcdefghijk',
|
||||
'123-456-7890',
|
||||
];
|
||||
return invalidPhones[Math.floor(Math.random() * invalidPhones.length)];
|
||||
}
|
||||
|
||||
static generateShortMessage(): string {
|
||||
return '测试';
|
||||
}
|
||||
|
||||
static generateLongMessage(): string {
|
||||
return 'A'.repeat(1001);
|
||||
}
|
||||
|
||||
static generateSpecialCharacters(): string {
|
||||
return '!@#$%^&*()_+-=[]{}|;:,.<>?/~`';
|
||||
}
|
||||
|
||||
static generateChineseCharacters(): string {
|
||||
return '这是一段中文测试文本,包含了一些特殊字符:!@#¥%……&*()——+';
|
||||
}
|
||||
|
||||
static generateMixedContent(): string {
|
||||
return 'Hello 世界!This is a mixed content test 测试。';
|
||||
}
|
||||
|
||||
static generateNumberString(length: number): string {
|
||||
return Math.random().toString().slice(2, 2 + length);
|
||||
}
|
||||
|
||||
static generateAlphanumeric(length: number): string {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static generateWhitespace(): string {
|
||||
return ' \t\n\r ';
|
||||
}
|
||||
|
||||
static generateUrl(): string {
|
||||
const urls = [
|
||||
'https://example.com',
|
||||
'http://test.com',
|
||||
'https://demo.com/path',
|
||||
'http://example.com/page?param=value',
|
||||
];
|
||||
return urls[Math.floor(Math.random() * urls.length)];
|
||||
}
|
||||
|
||||
static generateDate(): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + Math.floor(Math.random() * 30));
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
static generateTime(): string {
|
||||
const hours = Math.floor(Math.random() * 24);
|
||||
const minutes = Math.floor(Math.random() * 60);
|
||||
return `${hours.toString().padStart(2, '0')}:${minutes.toString().padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
static generateBoolean(): boolean {
|
||||
return Math.random() < 0.5;
|
||||
}
|
||||
|
||||
static generateNumber(min: number = 0, max: number = 100): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
static generateFloat(min: number = 0, max: number = 100, decimals: number = 2): number {
|
||||
const num = Math.random() * (max - min) + min;
|
||||
return parseFloat(num.toFixed(decimals));
|
||||
}
|
||||
|
||||
static generateArray<T>(generator: () => T, length: number = 5): T[] {
|
||||
return Array.from({ length }, generator);
|
||||
}
|
||||
|
||||
static generateObject(): Record<string, any> {
|
||||
return {
|
||||
name: this.generateName(),
|
||||
email: this.generateEmail(),
|
||||
age: this.generateNumber(18, 65),
|
||||
active: this.generateBoolean(),
|
||||
score: this.generateFloat(0, 100),
|
||||
};
|
||||
}
|
||||
|
||||
static generateUuid(): string {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||
const r = Math.random() * 16 | 0;
|
||||
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||
return v.toString(16);
|
||||
});
|
||||
}
|
||||
|
||||
static generateIPv4(): string {
|
||||
return `${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}.${this.generateNumber(0, 255)}`;
|
||||
}
|
||||
|
||||
static generateMacAddress(): string {
|
||||
const hex = '0123456789ABCDEF';
|
||||
let mac = '';
|
||||
for (let i = 0; i < 6; i++) {
|
||||
if (i > 0) mac += ':';
|
||||
mac += hex[Math.floor(Math.random() * 16)];
|
||||
mac += hex[Math.floor(Math.random() * 16)];
|
||||
}
|
||||
return mac;
|
||||
}
|
||||
|
||||
static generateColor(): string {
|
||||
return `#${this.generateAlphanumeric(6)}`;
|
||||
}
|
||||
|
||||
static generateJson(): string {
|
||||
const obj = this.generateObject();
|
||||
return JSON.stringify(obj);
|
||||
}
|
||||
|
||||
static generateXml(): string {
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<root>
|
||||
<name>${this.generateName()}</name>
|
||||
<email>${this.generateEmail()}</email>
|
||||
<age>${this.generateNumber(18, 65)}</age>
|
||||
</root>`;
|
||||
}
|
||||
|
||||
static generateCsv(): string {
|
||||
const headers = ['Name,Email,Phone,Company'];
|
||||
const rows = this.generateArray(() =>
|
||||
`${this.generateName()},${this.generateEmail()},${this.generatePhone()},${this.generateCompany()}`,
|
||||
5
|
||||
);
|
||||
return [...headers, ...rows].join('\n');
|
||||
}
|
||||
|
||||
static generateHtml(): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Test Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>${this.generateName()}</h1>
|
||||
<p>${this.generateMessage()}</p>
|
||||
</body>
|
||||
</html>`;
|
||||
}
|
||||
|
||||
static generateMarkdown(): string {
|
||||
return `# ${this.generateName()}
|
||||
|
||||
## Contact Information
|
||||
- Email: ${this.generateEmail()}
|
||||
- Phone: ${this.generatePhone()}
|
||||
|
||||
## Message
|
||||
${this.generateMessage()}`;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { DeviceConfig } from '../types';
|
||||
|
||||
export const devices: Record<string, DeviceConfig> = {
|
||||
'desktop-1920x1080': {
|
||||
name: 'Desktop 1920x1080',
|
||||
viewport: { width: 1920, height: 1080 },
|
||||
isMobile: false,
|
||||
},
|
||||
'desktop-1366x768': {
|
||||
name: 'Desktop 1366x768',
|
||||
viewport: { width: 1366, height: 768 },
|
||||
isMobile: false,
|
||||
},
|
||||
'desktop-1280x720': {
|
||||
name: 'Desktop 1280x720',
|
||||
viewport: { width: 1280, height: 720 },
|
||||
isMobile: false,
|
||||
},
|
||||
'laptop-1440x900': {
|
||||
name: 'Laptop 1440x900',
|
||||
viewport: { width: 1440, height: 900 },
|
||||
isMobile: false,
|
||||
},
|
||||
'laptop-1024x768': {
|
||||
name: 'Laptop 1024x768',
|
||||
viewport: { width: 1024, height: 768 },
|
||||
isMobile: false,
|
||||
},
|
||||
'tablet-768x1024': {
|
||||
name: 'Tablet 768x1024',
|
||||
viewport: { width: 768, height: 1024 },
|
||||
isMobile: true,
|
||||
},
|
||||
'tablet-834x1194': {
|
||||
name: 'Tablet 834x1194 (iPad Pro)',
|
||||
viewport: { width: 834, height: 1194 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-375x667': {
|
||||
name: 'Mobile 375x667 (iPhone SE)',
|
||||
viewport: { width: 375, height: 667 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-390x844': {
|
||||
name: 'Mobile 390x844 (iPhone 12)',
|
||||
viewport: { width: 390, height: 844 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-414x896': {
|
||||
name: 'Mobile 414x896 (iPhone 11)',
|
||||
viewport: { width: 414, height: 896 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-360x640': {
|
||||
name: 'Mobile 360x640 (Android)',
|
||||
viewport: { width: 360, height: 640 },
|
||||
isMobile: true,
|
||||
},
|
||||
'mobile-412x915': {
|
||||
name: 'Mobile 412x915 (Pixel 5)',
|
||||
viewport: { width: 412, height: 915 },
|
||||
isMobile: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const desktopDevices = Object.entries(devices)
|
||||
.filter(([_, config]) => !config.isMobile)
|
||||
.map(([key, config]) => ({ key, ...config }));
|
||||
|
||||
export const mobileDevices = Object.entries(devices)
|
||||
.filter(([_, config]) => config.isMobile)
|
||||
.map(([key, config]) => ({ key, ...config }));
|
||||
|
||||
export const tabletDevices = Object.entries(devices)
|
||||
.filter(([_, config]) => config.isMobile && config.viewport.width >= 768)
|
||||
.map(([key, config]) => ({ key, ...config }));
|
||||
|
||||
export const getDevice = (key: string): DeviceConfig => {
|
||||
return devices[key] || devices['desktop-1280x720'];
|
||||
};
|
||||
|
||||
export const getAllDevices = (): DeviceConfig[] => {
|
||||
return Object.values(devices);
|
||||
};
|
||||
|
||||
export const getDesktopDevices = (): DeviceConfig[] => {
|
||||
return desktopDevices.map(d => devices[d.key]);
|
||||
};
|
||||
|
||||
export const getMobileDevices = (): DeviceConfig[] => {
|
||||
return mobileDevices.map(d => devices[d.key]);
|
||||
};
|
||||
|
||||
export const getTabletDevices = (): DeviceConfig[] => {
|
||||
return tabletDevices.map(d => devices[d.key]);
|
||||
};
|
||||
|
||||
export const getBreakpoints = () => {
|
||||
return {
|
||||
xs: 0,
|
||||
sm: 640,
|
||||
md: 768,
|
||||
lg: 1024,
|
||||
xl: 1280,
|
||||
'2xl': 1536,
|
||||
};
|
||||
};
|
||||
|
||||
export const isMobile = (width: number): boolean => {
|
||||
const breakpoints = getBreakpoints();
|
||||
return width < breakpoints.lg;
|
||||
};
|
||||
|
||||
export const isTablet = (width: number): boolean => {
|
||||
const breakpoints = getBreakpoints();
|
||||
return width >= breakpoints.md && width < breakpoints.lg;
|
||||
};
|
||||
|
||||
export const isDesktop = (width: number): boolean => {
|
||||
const breakpoints = getBreakpoints();
|
||||
return width >= breakpoints.lg;
|
||||
};
|
||||
Reference in New Issue
Block a user