e2e-testing — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited e2e-testing (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
@rules/code-testing/general.mdc for the deterministic-test contract: tests must be reliable, isolated, and free of arbitrary sleeps.data-testid over CSS/structure.waitForTimeout. Rely on auto-waiting locators and explicit conditions.Check whether Playwright is actually present:
ls playwright.config.* 2>/dev/null
grep -q '@playwright/test' package.json 2>/dev/null && echo "playwright present"playwright.config.* file nor @playwright/test in package.json exists, STOP. Do not install Playwright. Defer to:@skills/test-like-human/SKILL.md for manual, scenario-based testing, ortests/
e2e/
auth/login.spec.ts
features/search.spec.ts
pages/
ItemsPage.ts
fixtures/
auth.ts
playwright.config.tsEncapsulate page structure so specs read as behavior, not selectors.
import { Page, Locator } from '@playwright/test'
export class ItemsPage {
readonly page: Page
readonly searchInput: Locator
readonly itemCards: Locator
readonly createButton: Locator
constructor(page: Page) {
this.page = page
// Prefer role/test-id selectors. data-testid is stable across restyles.
this.searchInput = page.getByTestId('search-input')
this.itemCards = page.getByTestId('item-card')
this.createButton = page.getByRole('button', { name: 'Create' })
}
async goto() {
await this.page.goto('/items')
}
async search(query: string) {
await this.searchInput.fill(query)
// Wait for the real response, not a fixed delay.
await this.page.waitForResponse(r => r.url().includes('/items') && r.ok())
}
itemCount() {
return this.itemCards.count()
}
}Order of preference:
getByRole(...) with an accessible name.getByTestId(...) — add data-testid to the relevant Blade/Livewire markup.getByLabel / getByText for forms and copy.import { test, expect } from '@playwright/test'
import { ItemsPage } from '../pages/ItemsPage'
test.describe('Item search', () => {
let items: ItemsPage
test.beforeEach(async ({ page }) => {
items = new ItemsPage(page)
await items.goto()
})
test('searches by keyword', async () => {
await items.search('test')
expect(await items.itemCount()).toBeGreaterThan(0)
await expect(items.itemCards.first()).toContainText(/test/i)
})
test('shows empty state with no results', async ({ page }) => {
await items.search('zzz-nonexistent')
await expect(page.getByTestId('no-results')).toBeVisible()
expect(await items.itemCount()).toBe(0)
})
})Point Playwright at the running app and let it boot php artisan serve (or use an already-running instance / CI app URL).
import { defineConfig, devices } from '@playwright/test'
export default defineConfig({
testDir: './tests/e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
reporter: [['html', { outputFolder: 'playwright-report' }], ['junit', { outputFile: 'playwright-results.xml' }]],
use: {
baseURL: process.env.APP_URL ?? 'http://127.0.0.1:8000',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
video: 'retain-on-failure',
},
projects: [
{ name: 'chromium', use: { ...devices['Desktop Chrome'] } },
{ name: 'mobile-chrome', use: { ...devices['Pixel 5'] } },
],
webServer: {
command: 'php artisan serve --port=8000',
url: 'http://127.0.0.1:8000',
reuseExistingServer: !process.env.CI,
timeout: 120_000,
},
})Use a dedicated testing database/env (.env.testing) so E2E runs never touch development data, and migrate/seed it before the suite.
Log in once, save storage state, and reuse it so specs skip the login flow.
// global-setup.ts — sign in once, persist cookies/session
import { chromium } from '@playwright/test'
export default async () => {
const browser = await chromium.launch()
const page = await browser.newPage()
await page.goto(`${process.env.APP_URL ?? 'http://127.0.0.1:8000'}/login`)
await page.getByLabel('Email').fill(process.env.E2E_EMAIL!)
await page.getByLabel('Password').fill(process.env.E2E_PASSWORD!)
await page.getByRole('button', { name: 'Log in' }).click()
await page.waitForURL('**/dashboard')
await page.context().storageState({ path: 'tests/e2e/.auth/user.json' })
await browser.close()
}Wire it via globalSetup and use: { storageState: 'tests/e2e/.auth/user.json' }. Keep .auth/ out of version control. You can also stub backend responses with page.route(...) to isolate the frontend from flaky external services.
name: E2E
on: [push, pull_request]
jobs:
e2e:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: '8.3' }
- run: composer install --no-interaction --prefer-dist
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npx playwright install --with-deps
- run: npm run build
- run: php artisan migrate --seed --env=testing
- run: npx playwright test
env:
APP_URL: http://127.0.0.1:8000
- uses: actions/upload-artifact@v4
if: always()
with: { name: playwright-report, path: playwright-report/, retention-days: 30 }Per @rules/code-testing/general.mdc, tests must be deterministic.
locator.click()), never page.click(selector) on a maybe-not-ready node.await page.waitForTimeout(...) with waitForResponse, waitForURL, or expect(locator).toBeVisible().await locator.waitFor({ state: 'visible' }).test.fixme(true, 'flaky — issue #NNN') and link an issue rather than leaving them red.npx playwright test path.spec.ts --repeat-each=10.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.