Migrate from AngularJS to Angular using hybrid mode, incremental component rewriting, and dependency injection updates. Use when upgrading AngularJS applications, planning framework migrations, or modernizing legacy Angular code.
中文简介正在准备中。当前显示原文说明。
PlaywrightとCypressを使用したエンドツーエンドテストをマスターし、バグを捕捉し、信頼性を向上させ、高速デプロイメントを可能にする信頼性の高いテストスイートを構築。E2Eテストの実装、不安定なテストのデバッグ、テスト基準の確立時に使用。
中文简介正在准备中。当前显示原文说明。
安装前可以查看交给 AI 代理的指令内容。
English | 日本語
迅速なコード出荷の信頼性を提供し、ユーザーより先にリグレッションを捕捉する、信頼性が高く、高速で、保守可能なエンドツーエンドテストスイートを構築します。
E2Eでテストすべきもの:
E2Eでテストすべきでないもの:
テストピラミッド:
/\
/E2E\ ← 少数、重要なパスに焦点
/─────\
/統合 \ ← より多く、コンポーネント間のやり取りをテスト
/────────\
/ユニットテスト\ ← 多数、高速、分離
/────────────\
ベストプラクティス:
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
timeout: 30000,
expect: {
timeout: 5000,
},
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['junit', { outputFile: 'results.xml' }],
],
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] } },
{ name: 'webkit', use: { ...devices['Desktop Safari'] } },
{ name: 'mobile', use: { ...devices['iPhone 13'] } },
],
});
// pages/LoginPage.ts
import { Page, Locator } from '@playwright/test';
export class LoginPage {
readonly page: Page;
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.loginButton = page.getByRole('button', { name: 'Login' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
await this.page.goto('/login');
}
async login(email: string, password: string) {
await this.emailInput.fill(email);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
async getErrorMessage(): Promise<string> {
return await this.errorMessage.textContent() ?? '';
}
}
// ページオブジェクトを使用したテスト
import { test, expect } from '@playwright/test';
import { LoginPage } from './pages/LoginPage';
test('ログイン成功', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await expect(page).toHaveURL('/dashboard');
await expect(page.getByRole('heading', { name: 'Dashboard' }))
.toBeVisible();
});
test('ログイン失敗時エラー表示', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('invalid@example.com', 'wrong');
const error = await loginPage.getErrorMessage();
expect(error).toContain('Invalid credentials');
});
// fixtures/test-data.ts
import { test as base } from '@playwright/test';
type TestData = {
testUser: {
email: string;
password: string;
name: string;
};
adminUser: {
email: string;
password: string;
};
};
export const test = base.extend<TestData>({
testUser: async ({}, use) => {
const user = {
email: `test-${Date.now()}@example.com`,
password: 'Test123!@#',
name: 'Test User',
};
// セットアップ:データベースにユーザーを作成
await createTestUser(user);
await use(user);
// クリーンアップ:ユーザーを削除
await deleteTestUser(user.email);
},
adminUser: async ({}, use) => {
await use({
email: 'admin@example.com',
password: process.env.ADMIN_PASSWORD!,
});
},
});
// テストでの使用
import { test } from './fixtures/test-data';
test('ユーザーがプロフィールを更新できる', async ({ page, testUser }) => {
await page.goto('/login');
await page.getByLabel('Email').fill(testUser.email);
await page.getByLabel('Password').fill(testUser.password);
await page.getByRole('button', { name: 'Login' }).click();
await page.goto('/profile');
await page.getByLabel('Name').fill('Updated Name');
await page.getByRole('button', { name: 'Save' }).click();
await expect(page.getByText('Profile updated')).toBeVisible();
});
// ❌ 悪い例:固定タイムアウト
await page.waitForTimeout(3000); // 不安定!
// ✅ 良い例:特定の条件を待つ
await page.waitForLoadState('networkidle');
await page.waitForURL('/dashboard');
await page.waitForSelector('[data-testid=\"user-profile\"]');
// ✅ より良い例:アサーション付き自動待機
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByRole('button', { name: 'Submit' }))
.toBeEnabled();
// APIレスポンスを待つ
const responsePromise = page.waitForResponse(
response => response.url().includes('/api/users') && response.status() === 200
);
await page.getByRole('button', { name: 'Load Users' }).click();
const response = await responsePromise;
const data = await response.json();
expect(data.users).toHaveLength(10);
// 複数条件を待つ
await Promise.all([
page.waitForURL('/success'),
page.waitForLoadState('networkidle'),
expect(page.getByText('Payment successful')).toBeVisible(),
]);
// APIレスポンスをモック
test('API失敗時エラー表示', async ({ page }) => {
await page.route('**/api/users', route => {
route.fulfill({
status: 500,
contentType: 'application/json',
body: JSON.stringify({ error: 'Internal Server Error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Failed to load users')).toBeVisible();
});
// リクエストをインターセプトして変更
test('APIリクエストを変更可能', async ({ page }) => {
await page.route('**/api/users', async route => {
const request = route.request();
const postData = JSON.parse(request.postData() || '{}');
// リクエストを変更
postData.role = 'admin';
await route.continue({
postData: JSON.stringify(postData),
});
});
// テスト継続...
});
// サードパーティサービスをモック
test('モックStripeで決済フロー', async ({ page }) => {
await page.route('**/api/stripe/**', route => {
route.fulfill({
status: 200,
body: JSON.stringify({
id: 'mock_payment_id',
status: 'succeeded',
}),
});
});
// モックレスポンスで決済フローをテスト
});
// cypress.config.ts
import { defineConfig } from 'cypress';
export default defineConfig({
e2e: {
baseUrl: 'http://localhost:3000',
viewportWidth: 1280,
viewportHeight: 720,
video: false,
screenshotOnRunFailure: true,
defaultCommandTimeout: 10000,
requestTimeout: 10000,
setupNodeEvents(on, config) {
// ノードイベントリスナーを実装
},
},
});
// cypress/support/commands.ts
declare global {
namespace Cypress {
interface Chainable {
login(email: string, password: string): Chainable<void>;
createUser(userData: UserData): Chainable<User>;
dataCy(value: string): Chainable<JQuery<HTMLElement>>;
}
}
}
Cypress.Commands.add('login', (email: string, password: string) => {
cy.visit('/login');
cy.get('[data-testid=\"email\"]').type(email);
cy.get('[data-testid=\"password\"]').type(password);
cy.get('[data-testid=\"login-button\"]').click();
cy.url().should('include', '/dashboard');
});
Cypress.Commands.add('createUser', (userData: UserData) => {
return cy.request('POST', '/api/users', userData)
.its('body');
});
Cypress.Commands.add('dataCy', (value: string) => {
return cy.get(`[data-cy=\"${value}\"]`);
});
// 使用例
cy.login('user@example.com', 'password');
cy.dataCy('submit-button').click();
// API呼び出しをモック
cy.intercept('GET', '/api/users', {
statusCode: 200,
body: [
{ id: 1, name: 'John' },
{ id: 2, name: 'Jane' },
],
}).as('getUsers');
cy.visit('/users');
cy.wait('@getUsers');
cy.get('[data-testid=\"user-list\"]').children().should('have.length', 2);
// レスポンスを変更
cy.intercept('GET', '/api/users', (req) => {
req.reply((res) => {
// レスポンスを変更
res.body.users = res.body.users.slice(0, 5);
res.send();
});
});
// 遅いネットワークをシミュレート
cy.intercept('GET', '/api/data', (req) => {
req.reply((res) => {
res.delay(3000); // 3秒遅延
res.send();
});
});
// Playwrightで
import { test, expect } from '@playwright/test';
test('ホームページが正しく表示される', async ({ page }) => {
await page.goto('/');
await expect(page).toHaveScreenshot('homepage.png', {
fullPage: true,
maxDiffPixels: 100,
});
});
test('ボタンの全状態', async ({ page }) => {
await page.goto('/components');
const button = page.getByRole('button', { name: 'Submit' });
// デフォルト状態
await expect(button).toHaveScreenshot('button-default.png');
// ホバー状態
await button.hover();
await expect(button).toHaveScreenshot('button-hover.png');
// 無効状態
await button.evaluate(el => el.setAttribute('disabled', 'true'));
await expect(button).toHaveScreenshot('button-disabled.png');
});
// playwright.config.ts
export default defineConfig({
projects: [
{
name: 'shard-1',
use: { ...devices['Desktop Chrome'] },
grepInvert: /@slow/,
shard: { current: 1, total: 4 },
},
{
name: 'shard-2',
use: { ...devices['Desktop Chrome'] },
shard: { current: 2, total: 4 },
},
// ... その他のシャード
],
});
// CIで実行
// npx playwright test --shard=1/4
// npx playwright test --shard=2/4
// インストール: npm install @axe-core/playwright
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';
test('ページにアクセシビリティ違反がないこと', async ({ page }) => {
await page.goto('/');
const accessibilityScanResults = await new AxeBuilder({ page })
.exclude('#third-party-widget')
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
test('フォームがアクセシブル', async ({ page }) => {
await page.goto('/signup');
const results = await new AxeBuilder({ page })
.include('form')
.analyze();
expect(results.violations).toEqual([]);
});
data-testidまたはdata-cy// ❌ 悪いセレクター
cy.get('.btn.btn-primary.submit-button').click();
cy.get('div > form > div:nth-child(2) > input').type('text');
// ✅ 良いセレクター
cy.getByRole('button', { name: 'Submit' }).click();
cy.getByLabel('Email address').type('user@example.com');
cy.get('[data-testid=\"email-input\"]').type('user@example.com');
// Playwrightデバッグ
// 1. ヘッドモードで実行
npx playwright test --headed
// 2. デバッグモードで実行
npx playwright test --debug
// 3. トレースビューアーを使用
await page.screenshot({ path: 'screenshot.png' });
await page.video()?.saveAs('video.webm');
// 4. より良いレポートのためにtest.stepを追加
test('チェックアウトフロー', async ({ page }) => {
await test.step('カートにアイテムを追加', async () => {
await page.goto('/products');
await page.getByRole('button', { name: 'Add to Cart' }).click();
});
await test.step('チェックアウトに進む', async () => {
await page.goto('/cart');
await page.getByRole('button', { name: 'Checkout' }).click();
});
});
// 5. ページ状態を検査
await page.pause(); // 実行を一時停止、インスペクターを開く
还没有评价。欢迎分享你的使用感受。
无需登录即可发表。欢迎分享使用感受、提问,或说明在哪些环境下无法运行。
还没有评论。来写第一条吧。
Migrate from AngularJS to Angular using hybrid mode, incremental component rewriting, and dependency injection updates. Use when upgrading AngularJS applications, planning framework migrations, or modernizing legacy Angular code.
中文简介正在准备中。当前显示原文说明。
ハイブリッドモード、段階的コンポーネント書き換え、依存性注入更新を使用してAngularJSからAngularへ移行します。AngularJSアプリケーションのアップグレード、フレームワーク移行の計画、またはレガシーAngularコードのモダナイゼーション時に使用します。
中文简介正在准备中。当前显示原文说明。
開発者を喜ばせる直感的でスケーラブルで保守可能なAPIを構築するために、RESTとGraphQL APIの設計原則をマスターします。新しいAPIの設計、API仕様のレビュー、またはAPI設計標準の確立時に使用してください。
中文简介正在准备中。当前显示原文说明。
Master REST and GraphQL API design principles to build intuitive, scalable, and maintainable APIs that delight developers. Use when designing new APIs, reviewing API specifications, or establishing API design standards.
中文简介正在准备中。当前显示原文说明。
Implement proven backend architecture patterns including Clean Architecture, Hexagonal Architecture, and Domain-Driven Design. Use when architecting complex backend systems or refactoring existing applications for better maintainability.
中文简介正在准备中。当前显示原文说明。
クリーンアーキテクチャ、ヘキサゴナルアーキテクチャ、ドメイン駆動設計を含む実証済みのバックエンドアーキテクチャパターンを実装します。複雑なバックエンドシステムの設計や、保守性向上のための既存アプリケーションのリファクタリング時に使用します。
中文简介正在准备中。当前显示原文说明。