返回 Skill 列表
extension
分类: 开发与工程无需 API Key

qa-validation-asset-loading

使用Playwright进行资产加载性能验证。当验证FBX模型加载性能和内存使用、测试不同环境下的资产加载、确保对失败的资产加载进行适当的错误处理,或验证资产格式的浏览器兼容性时,请使用此方法。

person作者: jakexiaohubgithub

Asset Loading Validation

When to Use

  • Validating FBX model loading performance and memory usage
  • Testing asset loading across different environments
  • Ensuring proper error handling for failed asset loads
  • Verifying browser compatibility for asset formats

Quick Start

Asset Loading Performance Test

// playwright.config.ts
export default defineConfig({
  // ... other config
  expect: {
    toHaveScreenshot: {
      maxPixelRatio: 1,
      threshold: 0.1,
    },
  },
});

// test/asset-loading.spec.ts
import { test, expect } from '../fixtures';

test.describe('Asset Loading Validation', () => {
  test('Sequential FBX model loading', async ({ page }) => {
    await page.goto('/');

    // Monitor memory usage during loading
    const memoryUsage = await page.evaluate(() => {
      return {
        jsHeapSizeLimit: performance.memory.jsHeapSizeLimit,
        totalJSHeapSize: performance.memory.totalJSHeapSize,
        usedJSHeapSize: performance.memory.usedJSHeapSize,
      };
    });

    // Check for loading indicators
    await expect(page.locator('.loading-indicator')).toBeVisible();

    // Wait for loading complete
    await expect(page.locator('.loading-indicator')).toBeHidden();

    // Verify models are loaded
    await expect(page.locator('canvas')).toBeVisible();
    await expect(page.locator('[data-testid="character-model"]')).toHaveCount(6);

    // Performance assertions
    const afterLoadMemory = await page.evaluate(() => performance.memory.usedJSHeapSize);
    const memoryIncrease = afterLoadMemory - memoryUsage.usedJSHeapSize;

    expect(memoryIncrease).toBeLessThan(100 * 1024 * 1024); // Less than 100MB increase
  });

  test('Error handling for missing assets', async ({ page }) => {
    await page.goto('/test-error-loading');

    await expect(page.locator('.error-message')).toBeVisible();
    await expect(page.locator('.error-message')).toContainText('Failed to load character');

    // Console check for errors
    const consoleLogs = await page.evaluate(() => {
      return (window as any).__consoleLogs || [];
    });

    expect(consoleLogs).not.toContain('error');
    expect(consoleLogs).toContain('asset loading failed');
  });
});

Browser Compatibility Test

test.describe('Cross-Browser Asset Loading', () => {
  ['chromium', 'firefox', 'webkit'].forEach((browserName) => {
    test(`Asset loading in ${browserName}`, async ({ page }) => {
      await page.goto('/');

      // Wait for initial load
      await page.waitForLoadState('networkidle');

      // Test asset loading in different browsers
      const assetResponses = await page.waitForResponse((response) =>
        response.url().includes('/assets/')
      );

      expect(assetResponses.status()).toBe(200);

      // Visual regression check
      await expect(page).toHaveScreenshot(`asset-loading-${browserName}.png`, {
        maxDiffPixels: 100,
      });
    });
  });
});

Anti-Patterns

DON'T: Only test assets in development environment

// Bad - Only tests local development
test('Asset loading', async ({ page }) => {
  await page.goto('http://localhost:3000'); // E2E tests use baseURL from playwright.config.ts
  // ... tests
});

DO: Test in multiple environments with different configurations

// Good - Tests multiple environments
[process.env.TEST_ENV].forEach((env) => {
  test(`Asset loading in ${env}`, async ({ page }) => {
    const baseUrl = env === 'production' ? 'https://your-game.com' : 'http://localhost:3000';

    await page.goto(`${baseUrl}/characters`);
    // ... environment-specific tests
  });
});

DON'T: Ignore memory usage during asset loading

// Bad - No memory monitoring
test('Load models', async ({ page }) => {
  await page.goto('/');
  await page.waitForSelector('[data-testid="model-loaded"]');
  // No memory checks
});

DO: Monitor memory and performance metrics

test('Memory-safe asset loading', async ({ page }) => {
  const initialMemory = await page.evaluate(() => {
    return performance.memory?.usedJSHeapSize || 0;
  });

  await page.goto('/characters');
  await page.waitForSelector('[data-testid="all-models-loaded"]');

  const finalMemory = await page.evaluate(() => {
    return performance.memory?.usedJSHeapSize || 0;
  });

  const memoryIncrease = finalMemory - initialMemory;
  expect(memoryIncrease).toBeLessThan(50 * 1024 * 1024); // 50MB limit

  // Check for memory leaks
  await page.waitForTimeout(5000);
  const afterWaitMemory = await page.evaluate(() => {
    return performance.memory?.usedJSHeapSize || 0;
  });

  expect(afterWaitMemory - finalMemory).toBeLessThan(5 * 1024 * 1024); // < 5MB after 5s
});

Performance Validation Patterns

Load Time Measurement

test.describe('Asset Loading Performance', () => {
  test('Sequential loading load time', async ({ page }) => {
    const startTime = Date.now();

    await page.goto('/characters');
    await page.waitForSelector('[data-testid="all-characters-loaded"]');

    const loadTime = Date.now() - startTime;

    // Assert load time is reasonable
    expect(loadTime).toBeLessThan(10000); // 10 seconds max

    // Check loading progress
    const progressHistory = await page.evaluate(() => {
      return (window as any).__progressLog || [];
    });

    expect(progressHistory.length).toBeGreaterThan(0);
    expect(progressHistory[progressHistory.length - 1]).toBe(100);
  });
});

Resource Usage Monitoring

test('GPU memory usage during loading', async ({ page }) => {
  // Note: This requires browser extensions or specific APIs
  const gpuInfo = await page.evaluate(() => {
    return (navigator as any).gpu?.memoryInfo || {};
  });

  expect(gpuInfo.totalAvailableVram).toBeGreaterThan(0);
  expect(gpuInfo.usedVram).toBeLessThan(gpuInfo.totalAvailableVram * 0.8);
});

Error Handling Validation

Error Recovery Test

test.describe('Asset Loading Error Handling', () => {
  test('Graceful handling of missing assets', async ({ page }) => {
    // Mock a failed asset load
    await page.route('**/assets/missing.fbx', (route) => {
      route.abort('failed');
    });

    await page.goto('/test-missing-assets');

    // Check error display
    await expect(page.locator('.error-message')).toBeVisible();
    await expect(page.locator('.error-message')).not.toContain('crashed');

    // Check fallback content
    await expect(page.locator('.fallback-model')).toBeVisible();

    // Console error check
    const consoleErrors = await page.evaluate(() => {
      return (window as any).__consoleErrors || [];
    });

    expect(consoleErrors.length).toBe(0); // No unhandled errors
  });

  test('Retry mechanism for transient failures', async ({ page }) => {
    let attemptCount = 0;

    // Mock 2 failures then success
    await page.route('**/assets/retry-test.fbx', (route) => {
      attemptCount++;
      if (attemptCount < 3) {
        route.abort('failed');
      } else {
        route.fulfill({
          status: 200,
          contentType: 'application/octet-stream',
          body: new ArrayBuffer(1024), // Mock FBX data
        });
      }
    });

    await page.goto('/test-retry-mechanism');

    // Should eventually succeed
    await expect(page.locator('.model-loaded')).toBeVisible();
    expect(attemptCount).toBe(3); // 2 failures + 1 success
  });
});

Network Conditions Testing

test.describe('Network Condition Testing', () => {
  ['slow-3g', 'offline'].forEach((condition) => {
    test(`Asset loading under ${condition}`, async ({ page }) => {
      await page.route('**/*', (route) => {
        if (route.request().resourceType() === 'document') {
          route.continue();
        } else {
          route.abort();
        }
      });

      // Set network condition
      await page.emulateNetworkConditions({
        offline: condition === 'offline',
        downloadThroughput: condition === 'slow-3g' ? 500 * 1024 : 0,
        uploadThroughput: condition === 'slow-3g' ? 500 * 1024 : 0,
      });

      await page.goto('/characters');

      if (condition === 'offline') {
        await expect(page.locator('.offline-message')).toBeVisible();
      } else {
        await expect(page.locator('.loading-message')).toBeVisible();
      }
    });
  });
});

Visual Regression Testing

Loading States

test.describe('Visual Regression for Loading States', () => {
  test('Loading state appearance', async ({ page }) => {
    await page.goto('/characters');

    // Capture loading state
    await expect(page).toHaveScreenshot('loading-state.png', {
      mask: [page.locator('.loading-spinner')], // Animate elements
    });

    await page.waitForSelector('[data-testid="characters-loaded"]');

    // Capture loaded state
    await expect(page).toHaveScreenshot('loaded-state.png');
  });
});

Error State Visuals

test('Error state visual consistency', async ({ page }) => {
  // Mock asset loading failure
  await page.route('**/assets/error-test.fbx', (route) => {
    route.abort('failed');
  });

  await page.goto('/error-test');

  await expect(page).toHaveScreenshot('error-state.png');
  await expect(page.locator('.error-icon')).toBeVisible();
  await expect(page.locator('.retry-button')).toBeVisible();
});

Automated Performance Metrics

Performance Budget Checker

function createPerformanceBudget() {
  return {
    maxLoadTime: 10000, // 10 seconds
    maxMemoryIncrease: 50 * 1024 * 1024, // 50MB
    maxConcurrentLoads: 3,
    allowedErrors: 0,

    async checkBudget(page: Page, testId: string) {
      const metrics = await page.evaluate(() => {
        return {
          loadTime:
            window.performance.timing.loadEventEnd - window.performance.timing.navigationStart,
          memory: {
            used: performance.memory?.usedJSHeapSize || 0,
            total: performance.memory?.jsHeapSizeLimit || 0,
          },
          errors: (window as any).__consoleErrors?.length || 0,
        };
      });

      const violations = [];

      if (metrics.loadTime > this.maxLoadTime) {
        violations.push(`Load time exceeded: ${metrics.loadTime}ms > ${this.maxLoadTime}ms`);
      }

      if (metrics.memory.used > this.maxMemoryIncrease) {
        violations.push(`Memory increase exceeded: ${metrics.memory.used} bytes`);
      }

      if (metrics.errors > this.allowedErrors) {
        violations.push(`Error count exceeded: ${metrics.errors} errors`);
      }

      if (violations.length > 0) {
        throw new Error(`Performance budget violated:\n${violations.join('\n')}`);
      }

      return metrics;
    },
  };
}

// Usage in test
const budget = createPerformanceBudget();
test('Performance budget compliance', async ({ page }) => {
  await page.goto('/characters');
  await page.waitForSelector('[data-testid="all-loaded"]');

  await budget.checkBudget(page, 'sequential-loading-test');
});

Reference