소스 정보
- 저장소
- artofrawr/claude-control
- 최근 소스 활동
- 2026년 4월 9일 21:38
- 감지된 SKILL.md 언어
- 영어
- 스타
- 1
- 포크
- 0
설치 방법
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
소스 파일 검토
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
메뉴
기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.
설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/artofrawr/claude-control --skill playwright-testing명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
Process multimedia files with FFmpeg (video/audio encoding, conversion, streaming, filtering, hardware acceleration) and ImageMagick (image manipulation, format conversion, batch processing, effects, composition). Use when converting media formats, encoding videos with specific codecs (H.264, H.265, VP9), resizing/cropping images, extracting audio from video, applying filters and effects, optimizing file sizes, creating streaming manifests (HLS/DASH), generating thumbnails, batch processing images, creating composite images, or implementing media processing pipelines. Supports 100+ formats, hardware acceleration (NVENC, QSV), and complex filtergraphs.
Stripe Checkout, subscriptions, webhooks, customer portal
When the user wants to write, rewrite, or improve marketing copy for any page — including homepage, landing pages, pricing pages, feature pages, about pages, or product pages. Also use when the user says "write copy for," "improve this copy," "rewrite this page," "marketing copy," "headline help," "CTA copy," "value proposition," "tagline," "subheadline," "hero section copy," "above the fold," "this copy is weak," "make this more compelling," or "help me describe my product." Use this whenever someone is working on website text that needs to persuade or convert. For email copy, see email-sequence. For popup copy, see popup-cro. For editing existing copy, see copy-editing.
SKILL.md 표시 중
SOC 직업 분류 기준
| name | playwright-testing |
| description | E2E testing with Playwright - Page Objects, cross-browser, CI/CD |
| disable-model-invocation | false |
Load with: base.md + [framework].md
For end-to-end testing of web applications with Playwright - cross-browser, fast, reliable.
Sources: Playwright Best Practices | Playwright Docs | Better Stack Guide
# New project
npm init playwright@latest
# Existing project
npm install -D @playwright/test
npx playwright install
// playwright.config.ts
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [
['html'],
['list'],
process.env.CI ? ['github'] : ['line'],
],
use: {
baseURL: process.env.BASE_URL || 'http://localhost:3000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
// Auth setup - runs once before all tests
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
dependencies: ['setup'],
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
dependencies: ['setup'],
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
dependencies: ['setup'],
},
// Mobile viewports
{
name: 'mobile-chrome',
use: { ...devices['Pixel 5'] },
dependencies: ['setup'],
},
{
name: 'mobile-safari',
use: { ...devices['iPhone 12'] },
dependencies: ['setup'],
},
],
// Start dev server before tests
webServer: {
command: 'npm run dev',
url: 'http://localhost:3000',
reuseExistingServer: !process.env.CI,
timeout: 120 * 1000,
},
});
project/
├── e2e/
│ ├── fixtures/
│ │ ├── auth.fixture.ts # Auth fixtures
│ │ └── test.fixture.ts # Extended test with fixtures
│ ├── pages/
│ │ ├── base.page.ts # Base page object
│ │ ├── login.page.ts # Login page object
│ │ ├── dashboard.page.ts # Dashboard page object
│ │ └── index.ts # Export all pages
│ ├── tests/
│ │ ├── auth.spec.ts # Auth tests
│ │ ├── dashboard.spec.ts # Dashboard tests
│ │ └── checkout.spec.ts # Checkout flow tests
│ ├── utils/
│ │ ├── helpers.ts # Test helpers
│ │ └── test-data.ts # Test data factories
│ └── auth.setup.ts # Global auth setup
├── playwright.config.ts
└── .auth/ # Stored auth state (gitignored)
Use locators that mirror how users interact with the page:
// ✅ BEST: Role-based (accessible, resilient)
page.getByRole('button', { name: 'Submit' })
page.getByRole('textbox', { name: 'Email' })
page.getByRole('link', { name: 'Sign up' })
page.getByRole('heading', { name: 'Welcome' })
// ✅ GOOD: User-facing text
page.getByLabel('Email address')
page.getByPlaceholder('Enter your email')
page.getByText('Welcome back')
page.getByTitle('Profile settings')
// ✅ GOOD: Test IDs (stable, explicit)
page.getByTestId('submit-button')
page.getByTestId('user-avatar')
// ⚠️ AVOID: CSS selectors (brittle)
page.locator('.btn-primary')
page.locator('#submit')
// ❌ NEVER: XPath (extremely brittle)
page.locator('//div[@class="container"]/button[1]')
// Narrow down to specific section
const form = page.getByRole('form', { name: 'Login' });
await form.getByRole('textbox', { name: 'Email' }).fill('user@example.com');
await form.getByRole('button', { name: 'Submit' }).click();
// Filter within a list
const productCard = page.getByTestId('product-card')
.filter({ hasText: 'Pro Plan' });
await productCard.getByRole('button', { name: 'Buy' }).click();
// e2e/pages/base.page.ts
import { Page, Locator } from '@playwright/test';
export abstract class BasePage {
constructor(protected page: Page) {}
async navigate(path: string = '/') {
await this.page.goto(path);
}
async waitForPageLoad() {
await this.page.waitForLoadState('networkidle');
}
// Common elements
get header() {
return this.page.getByRole('banner');
}
get footer() {
return this.page.getByRole('contentinfo');
}
// Common actions
async clickNavLink(name: ) {
..(, { name }).();
}
}
// e2e/pages/login.page.ts
import { Page, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class LoginPage extends BasePage {
readonly emailInput: Locator;
readonly passwordInput: Locator;
readonly submitButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
super(page);
this.emailInput = page.getByLabel('Email');
this.passwordInput = page.getByLabel('Password');
this.submitButton = page.getByRole('button', { name: 'Sign in' });
this.errorMessage = page.getByRole('alert');
}
async goto() {
.();
}
() {
..(email);
..(password);
..();
}
() {
(.).(message);
}
() {
(.).();
}
}
// e2e/pages/dashboard.page.ts
import { Page, Locator, expect } from '@playwright/test';
import { BasePage } from './base.page';
export class DashboardPage extends BasePage {
readonly welcomeHeading: Locator;
readonly userMenu: Locator;
readonly logoutButton: Locator;
constructor(page: Page) {
super(page);
this.welcomeHeading = page.getByRole('heading', { name: /welcome/i });
this.userMenu = page.getByTestId('user-menu');
this.logoutButton = page.getByRole('button', { name: 'Logout' });
}
async goto() {
await this.navigate('/dashboard');
}
() {
..();
..();
}
() {
(.).(name);
}
}
// e2e/pages/index.ts
export { BasePage } from './base.page';
export { LoginPage } from './login.page';
export { DashboardPage } from './dashboard.page';
// e2e/auth.setup.ts
import { test as setup, expect } from '@playwright/test';
import path from 'path';
const authFile = path.join(__dirname, '../.auth/user.json');
setup('authenticate', async ({ page }) => {
// Go to login page
await page.goto('/login');
// Login with test credentials
await page.getByLabel('Email').fill(process.env.TEST_USER_EMAIL!);
await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
await page.getByRole('button', { name: 'Sign in' }).click();
// Wait for auth to complete
await expect(page).toHaveURL(/.*dashboard/);
// Save auth state for reuse
await page.context().storageState({ path: authFile });
});
// playwright.config.ts
export default defineConfig({
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{
name: 'chromium',
use: {
...devices['Desktop Chrome'],
storageState: '.auth/user.json',
},
dependencies: ['setup'],
},
],
});
// e2e/tests/public.spec.ts
import { test } from '@playwright/test';
// Override to skip auth
test.use({ storageState: { cookies: [], origins: [] } });
test('homepage loads for anonymous users', async ({ page }) => {
await page.goto('/');
await expect(page.getByRole('heading', { name: 'Welcome' })).toBeVisible();
});
// e2e/tests/auth.spec.ts
import { test, expect } from '@playwright/test';
import { LoginPage } from '../pages';
test.describe('Authentication', () => {
test.beforeEach(async ({ page }) => {
// Skip stored auth for login tests
await page.context().clearCookies();
});
test('successful login redirects to dashboard', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('user@example.com', 'password123');
await loginPage.expectLoggedIn();
});
test('invalid credentials show error', async ({ page }) => {
const loginPage = new LoginPage(page);
await loginPage.goto();
await loginPage.login('wrong@example.com', 'wrongpass');
await loginPage.expectError('Invalid email or password');
});
test(, ({ page }) => {
loginPage = (page);
loginPage.();
loginPage..();
(page.()).();
(page.()).();
});
});
// e2e/tests/checkout.spec.ts
import { test, expect } from '@playwright/test';
test.describe('Checkout Flow', () => {
test('complete purchase flow', async ({ page }) => {
// 1. Browse products
await page.goto('/products');
await page.getByTestId('product-card')
.filter({ hasText: 'Pro Plan' })
.getByRole('button', { name: 'Add to cart' })
.click();
// 2. View cart
await page.getByRole('link', { name: 'Cart' }).click();
await expect(page.getByText('Pro Plan')).toBeVisible();
await expect(page.getByTestId('cart-total')).toContainText('$29.99');
// 3. Checkout
await page.getByRole('button', { name: 'Checkout' }).click();
stripeFrame = page.();
stripeFrame.().();
stripeFrame.().();
stripeFrame.().();
page.(, { : }).();
(page).();
(page.(, { : })).();
});
});
// ✅ These wait and retry automatically
await expect(page.getByRole('button')).toBeVisible();
await expect(page.getByRole('button')).toBeEnabled();
await expect(page.getByRole('button')).toHaveText('Submit');
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle(/Dashboard/);
// ❌ Avoid manual waits
await page.waitForTimeout(3000); // NEVER do this
// Continue test even if assertion fails
await expect.soft(page.getByTestId('price')).toHaveText('$29.99');
await expect.soft(page.getByTestId('stock')).toHaveText('In Stock');
// Fail at end if any soft assertions failed
// Visibility
await expect(locator).toBeVisible();
await expect(locator).toBeHidden();
await expect(locator).toBeAttached();
// Text content
await expect(locator).toHaveText('exact text');
await expect(locator).toContainText('partial');
await expect(locator).toHaveValue('input value');
// State
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toBeChecked();
await expect(locator).toBeFocused();
// Count
await expect(locator).toHaveCount(5);
// Page
await expect(page).toHaveURL('/dashboard');
await expect(page).toHaveTitle('Dashboard | App');
(page).();
test('shows error when API fails', async ({ page }) => {
// Mock API to return error
await page.route('**/api/users', (route) => {
route.fulfill({
status: 500,
body: JSON.stringify({ error: 'Server error' }),
});
});
await page.goto('/users');
await expect(page.getByText('Failed to load users')).toBeVisible();
});
test('displays user data from API', async ({ page }) => {
// Mock successful response
await page.route('**/api/users', (route) => {
route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify([
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, : , : },
]),
});
});
page.();
(page.()).();
(page.()).();
});
test('submits form and shows success', async ({ page }) => {
await page.goto('/contact');
// Fill form
await page.getByLabel('Name').fill('John');
await page.getByLabel('Email').fill('john@example.com');
await page.getByLabel('Message').fill('Hello!');
// Wait for API call on submit
const responsePromise = page.waitForResponse('**/api/contact');
await page.getByRole('button', { name: 'Send' }).click();
const response = await responsePromise;
expect(response.status()).toBe(200);
await expect(page.getByText('Message sent!')).toBeVisible();
});
// Full page screenshot
await expect(page).toHaveScreenshot('homepage.png');
// Element screenshot
await expect(page.getByTestId('chart')).toHaveScreenshot('chart.png');
// With options
await expect(page).toHaveScreenshot('dashboard.png', {
maxDiffPixels: 100,
mask: [page.getByTestId('timestamp')], // Ignore dynamic content
});
# .github/workflows/e2e.yml
name: E2E Tests
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Run E2E tests
run: npx playwright test --project=chromium
env:
# Run all tests
npx playwright test
# Run specific file
npx playwright test e2e/tests/auth.spec.ts
# Run tests with tag
npx playwright test --grep @critical
# Run in headed mode (debug)
npx playwright test --headed
# Run specific browser
npx playwright test --project=chromium
# Debug mode
npx playwright test --debug
# Show HTML report
npx playwright show-report
// e2e/utils/test-data.ts
import { faker } from '@faker-js/faker';
export const createUser = (overrides = {}) => ({
email: faker.internet.email(),
password: faker.internet.password({ length: 12 }),
name: faker.person.fullName(),
...overrides,
});
export const createProduct = (overrides = {}) => ({
name: faker.commerce.productName(),
price: faker.commerce.price({ min: 10, max: 100 }),
description: faker.commerce.productDescription(),
...overrides,
});
# .env.test
BASE_URL=http://localhost:3000
TEST_USER_EMAIL=test@example.com
TEST_USER_PASSWORD=testpassword123
// Enable in config for failures
use: {
trace: 'on-first-retry',
}
// View traces
npx playwright show-trace trace.zip
# Step through test
npx playwright test --debug
# Pause at specific point
await page.pause(); // In test code
Install "Playwright Test for VS Code" for:
Every project MUST include dead link detection tests. Run these on every deployment.
// e2e/tests/links.spec.ts
import { test, expect } from '@playwright/test';
const PAGES_TO_CHECK = ['/', '/about', '/pricing', '/blog', '/contact'];
test.describe('Dead Link Detection', () => {
for (const pagePath of PAGES_TO_CHECK) {
test(`no dead links on ${pagePath}`, async ({ page, request }) => {
await page.goto(pagePath);
// Get all links on the page
const links = await page.locator('a[href]').all();
const hrefs = await Promise.all(
links.map(link => link.getAttribute('href'))
);
// Filter to internal and absolute external links
const uniqueLinks = [...new Set(hrefs.filter(Boolean))] as string[];
for (const href uniqueLinks) {
(href.() || href.() || href.()) {
;
}
url = href.() ? href : (href, page.()).;
response = request.(url, {
: ,
: ,
});
(
response.(),
).();
}
});
}
});
// e2e/tests/site-links.spec.ts
import { test, expect, Page, APIRequestContext } from '@playwright/test';
interface LinkResult {
url: string;
status: number;
foundOn: string;
}
async function checkAllLinks(
page: Page,
request: APIRequestContext,
startUrl: string
): Promise<LinkResult[]> {
const visited = new Set<string>();
const results: LinkResult[] = [];
const toVisit = [startUrl];
const baseUrl = new URL(startUrl).origin;
while (toVisit.length > 0) {
const currentUrl = toVisit.pop()!;
if (visited.has(currentUrl)) continue;
visited.add(currentUrl);
try {
await page.goto(currentUrl);
const links = page.().();
( link links) {
href = link.();
(!href || href.() || href.() || href.()) {
;
}
fullUrl = href.() ? href : (href, currentUrl).;
response = request.(fullUrl, {
: ,
: ,
});
results.({
: fullUrl,
: response.(),
: currentUrl,
});
(fullUrl.(baseUrl) && !visited.(fullUrl)) {
toVisit.(fullUrl);
}
}
} (error) {
results.({
: currentUrl,
: ,
: ,
});
}
}
results;
}
(, ({ page, request, baseURL }) => {
results = (page, request, baseURL!);
deadLinks = results.( r. >= || r. === );
(deadLinks. > ) {
.();
deadLinks.( {
.();
});
}
(deadLinks, ).();
});
// e2e/tests/images.spec.ts
import { test, expect } from '@playwright/test';
test('no broken images on homepage', async ({ page, request }) => {
await page.goto('/');
const images = await page.locator('img[src]').all();
for (const img of images) {
const src = await img.getAttribute('src');
if (!src) continue;
const url = src.startsWith('http') ? src : new URL(src, page.url()).href;
// Skip data URLs
if (url.startsWith('data:')) continue;
const response = await request.get(url);
expect(
response.ok(),
`Broken image: ${src}`
).toBeTruthy();
// Verify it's actually an image
const contentType = response.headers()['content-type'];
expect(
contentType?.(),
).();
}
});
# .github/workflows/link-check.yml
name: Link Check
on:
schedule:
- cron: '0 6 * * 1' # Weekly on Monday
push:
branches: [main]
jobs:
link-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx playwright install chromium
- run: npx playwright test e2e/tests/links.spec.ts --project=chromium
env:
BASE_URL: ${{ secrets.PRODUCTION_URL }}
no-floating-promises# Install
npm init playwright@latest
# Run tests
npx playwright test
npx playwright test --headed
npx playwright test --project=chromium
npx playwright test --grep @smoke
# Debug
npx playwright test --debug
npx playwright show-report
npx playwright show-trace trace.zip
# Generate tests
npx playwright codegen localhost:3000
{
"scripts": {
"test:e2e": "playwright test",
"test:e2e:headed": "playwright test --headed",
"test:e2e:debug": "playwright test --debug",
"test:e2e:report": "playwright show-report",
"test:e2e:codegen": "playwright codegen"
}
}