255 lines
9.1 KiB
TypeScript
255 lines
9.1 KiB
TypeScript
import { test, expect, type Page } from '@playwright/test'
|
|
import { execFileSync } from 'node:child_process'
|
|
import { existsSync } from 'node:fs'
|
|
import path from 'node:path'
|
|
|
|
type ReportingFixture = {
|
|
viewerEmail: string
|
|
viewerPassword: string
|
|
cardId: number
|
|
cardPath: string
|
|
}
|
|
|
|
const VITE_MANIFEST_PATH = path.join(process.cwd(), 'public', 'build', 'manifest.json')
|
|
|
|
function ensureCompiledAssets() {
|
|
if (existsSync(VITE_MANIFEST_PATH)) {
|
|
return
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
execFileSync('cmd.exe', ['/c', 'npm', 'run', 'build'], {
|
|
cwd: process.cwd(),
|
|
stdio: 'inherit',
|
|
})
|
|
|
|
return
|
|
}
|
|
|
|
execFileSync('npm', ['run', 'build'], {
|
|
cwd: process.cwd(),
|
|
stdio: 'inherit',
|
|
})
|
|
}
|
|
|
|
function ensureNovaCardSeedData() {
|
|
execFileSync('php', ['artisan', 'db:seed', '--class=Database\\Seeders\\NovaCardCategorySeeder', '--no-interaction'], {
|
|
cwd: process.cwd(),
|
|
stdio: 'inherit',
|
|
})
|
|
|
|
execFileSync('php', ['artisan', 'db:seed', '--class=Database\\Seeders\\NovaCardTemplateSeeder', '--no-interaction'], {
|
|
cwd: process.cwd(),
|
|
stdio: 'inherit',
|
|
})
|
|
}
|
|
|
|
function resetBotProtectionState() {
|
|
const script = [
|
|
'use Illuminate\\Support\\Facades\\DB;',
|
|
'use Illuminate\\Support\\Facades\\Schema;',
|
|
"foreach (['forum_bot_logs', 'forum_bot_ip_blacklist', 'forum_bot_device_fingerprints', 'forum_bot_behavior_profiles'] as $table) {",
|
|
' if (Schema::hasTable($table)) {',
|
|
' DB::table($table)->delete();',
|
|
' }',
|
|
'}',
|
|
"echo 'ok';",
|
|
].join(' ')
|
|
|
|
execFileSync('php', ['artisan', 'tinker', `--execute=${script}`], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
})
|
|
}
|
|
|
|
function seedReportingFixture(): ReportingFixture {
|
|
const token = `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`
|
|
const viewerEmail = `e2e-nova-report-${token}@example.test`
|
|
const viewerUsername = `nrviewer${token}`.slice(0, 20)
|
|
const creatorEmail = `e2e-nova-report-creator-${token}@example.test`
|
|
const creatorUsername = `nrcreator${token}`.slice(0, 20)
|
|
const cardSlug = `nova-report-card-${token}`
|
|
|
|
const script = [
|
|
'use App\\Models\\NovaCard;',
|
|
'use App\\Models\\NovaCardCategory;',
|
|
'use App\\Models\\NovaCardTemplate;',
|
|
'use App\\Models\\User;',
|
|
'use Illuminate\\Support\\Facades\\Hash;',
|
|
`$viewer = User::updateOrCreate(['email' => '${viewerEmail}'], [`,
|
|
" 'name' => 'E2E Nova Report Viewer',",
|
|
` 'username' => '${viewerUsername}',`,
|
|
" 'onboarding_step' => 'complete',",
|
|
" 'email_verified_at' => now(),",
|
|
" 'is_active' => 1,",
|
|
" 'password' => Hash::make('password'),",
|
|
']);',
|
|
`$creator = User::updateOrCreate(['email' => '${creatorEmail}'], [`,
|
|
" 'name' => 'E2E Nova Report Creator',",
|
|
` 'username' => '${creatorUsername}',`,
|
|
" 'onboarding_step' => 'complete',",
|
|
" 'email_verified_at' => now(),",
|
|
" 'is_active' => 1,",
|
|
" 'password' => Hash::make('password'),",
|
|
']);',
|
|
'$category = NovaCardCategory::query()->orderBy("id")->first();',
|
|
'$template = NovaCardTemplate::query()->orderBy("id")->first();',
|
|
'$card = NovaCard::query()->create([',
|
|
' "user_id" => $creator->id,',
|
|
' "category_id" => $category->id,',
|
|
' "template_id" => $template->id,',
|
|
" 'title' => 'Playwright reportable card',",
|
|
` 'slug' => '${cardSlug}',`,
|
|
" 'quote_text' => 'Reported via Playwright.',",
|
|
" 'format' => NovaCard::FORMAT_SQUARE,",
|
|
' "project_json" => [',
|
|
' "content" => ["title" => "Playwright reportable card", "quote_text" => "Reported via Playwright."],',
|
|
' "layout" => ["layout" => "quote_heavy", "position" => "center", "alignment" => "center", "padding" => "comfortable", "max_width" => "balanced"],',
|
|
' "typography" => ["font_preset" => "modern-sans", "text_color" => "#ffffff", "accent_color" => "#e0f2fe", "quote_size" => 72, "author_size" => 28, "letter_spacing" => 0, "line_height" => 1.2, "shadow_preset" => "soft"],',
|
|
' "background" => ["type" => "gradient", "gradient_preset" => "midnight-nova", "gradient_colors" => ["#0f172a", "#1d4ed8"], "overlay_style" => "dark-soft", "focal_position" => "center", "blur_level" => 0, "opacity" => 50],',
|
|
' "decorations" => [],',
|
|
' ],',
|
|
" 'render_version' => 2,",
|
|
" 'schema_version' => 2,",
|
|
" 'background_type' => 'gradient',",
|
|
" 'visibility' => NovaCard::VISIBILITY_PUBLIC,",
|
|
" 'status' => NovaCard::STATUS_PUBLISHED,",
|
|
" 'moderation_status' => NovaCard::MOD_APPROVED,",
|
|
" 'allow_download' => true,",
|
|
" 'allow_remix' => true,",
|
|
" 'published_at' => now()->subMinute(),",
|
|
']);',
|
|
'echo json_encode([',
|
|
' "viewerEmail" => $viewer->email,',
|
|
' "viewerPassword" => "password",',
|
|
' "cardId" => $card->id,',
|
|
' "cardPath" => "/cards/{$card->slug}-{$card->id}",',
|
|
']);',
|
|
].join(' ')
|
|
|
|
const raw = execFileSync('php', ['artisan', 'tinker', `--execute=${script}`], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
})
|
|
|
|
const jsonLine = raw
|
|
.split(/\r?\n/)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean)
|
|
.reverse()
|
|
.find((line) => line.startsWith('{') && line.endsWith('}'))
|
|
|
|
if (!jsonLine) {
|
|
throw new Error(`Unable to parse Nova Card reporting fixture JSON: ${raw}`)
|
|
}
|
|
|
|
return JSON.parse(jsonLine) as ReportingFixture
|
|
}
|
|
|
|
function reportCount(cardId: number, viewerEmail: string): number {
|
|
const script = [
|
|
'use App\\Models\\Report;',
|
|
'use App\\Models\\User;',
|
|
`$viewer = User::query()->where('email', '${viewerEmail}')->first();`,
|
|
'if (! $viewer) { echo 0; return; }',
|
|
`echo Report::query()->where('reporter_id', $viewer->id)->where('target_type', 'nova_card')->where('target_id', ${cardId})->count();`,
|
|
].join(' ')
|
|
|
|
const raw = execFileSync('php', ['artisan', 'tinker', `--execute=${script}`], {
|
|
cwd: process.cwd(),
|
|
encoding: 'utf8',
|
|
})
|
|
|
|
const parsed = Number.parseInt(raw.trim().split(/\s+/).pop() || '0', 10)
|
|
return Number.isNaN(parsed) ? 0 : parsed
|
|
}
|
|
|
|
async function login(page: Page, fixture: ReportingFixture) {
|
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
await page.goto('/login')
|
|
const emailField = page.locator('input[name="email"]')
|
|
const internalServerError = page.getByText('Internal Server Error')
|
|
|
|
await Promise.race([
|
|
emailField.waitFor({ state: 'visible', timeout: 8000 }),
|
|
internalServerError.waitFor({ state: 'visible', timeout: 8000 }),
|
|
])
|
|
|
|
if ((await internalServerError.isVisible().catch(() => false)) && !(await emailField.isVisible().catch(() => false))) {
|
|
throw new Error('Nova Cards reporting login failed because the login page returned an internal server error.')
|
|
}
|
|
|
|
await emailField.fill(fixture.viewerEmail)
|
|
await page.locator('input[name="password"]').fill(fixture.viewerPassword)
|
|
await page.getByRole('button', { name: 'Sign In' }).click()
|
|
|
|
try {
|
|
await page.waitForURL((url) => url.pathname !== '/login', { timeout: 8000, waitUntil: 'domcontentloaded' })
|
|
await expect(page.getByRole('button', { name: /E2E Nova Report Viewer/i })).toBeVisible()
|
|
return
|
|
} catch {
|
|
const suspiciousActivity = page.getByText('Suspicious activity detected.')
|
|
if (attempt === 0 && (await suspiciousActivity.isVisible().catch(() => false))) {
|
|
resetBotProtectionState()
|
|
continue
|
|
}
|
|
|
|
throw new Error('Nova Cards reporting login failed before reaching an authenticated page.')
|
|
}
|
|
}
|
|
}
|
|
|
|
test.describe('Nova Cards reporting', () => {
|
|
test.describe.configure({ mode: 'serial' })
|
|
|
|
let fixture: ReportingFixture
|
|
|
|
test.beforeAll(() => {
|
|
ensureCompiledAssets()
|
|
ensureNovaCardSeedData()
|
|
fixture = seedReportingFixture()
|
|
})
|
|
|
|
test.beforeEach(() => {
|
|
resetBotProtectionState()
|
|
})
|
|
|
|
test('authenticated viewers can submit a report from the public card page', async ({ page }) => {
|
|
await login(page, fixture)
|
|
await page.goto(fixture.cardPath, { waitUntil: 'domcontentloaded' })
|
|
|
|
await expect(page.locator('[data-card-report]')).toBeVisible()
|
|
|
|
const dialogExpectations = [
|
|
{ type: 'prompt', message: 'Why are you reporting this card?', value: 'Playwright report reason' },
|
|
{ type: 'prompt', message: 'Add extra details for moderators (optional)', value: 'Playwright detail for moderation context.' },
|
|
{ type: 'alert', message: 'Report submitted. Thank you.' },
|
|
]
|
|
|
|
let handledDialogs = 0
|
|
|
|
const dialogHandler = async (dialog: Parameters<Page['on']>[1] extends (event: infer T) => any ? T : never) => {
|
|
const expected = dialogExpectations[handledDialogs]
|
|
expect(expected).toBeTruthy()
|
|
expect(dialog.type()).toBe(expected.type)
|
|
expect(dialog.message()).toBe(expected.message)
|
|
|
|
if (expected.type === 'prompt') {
|
|
await dialog.accept(expected.value)
|
|
} else {
|
|
await dialog.accept()
|
|
}
|
|
|
|
handledDialogs += 1
|
|
}
|
|
|
|
page.on('dialog', dialogHandler)
|
|
|
|
await page.locator('[data-card-report]').click()
|
|
|
|
await expect.poll(() => handledDialogs, { timeout: 10000 }).toBe(3)
|
|
await expect.poll(() => reportCount(fixture.cardId, fixture.viewerEmail), { timeout: 10000 }).toBe(1)
|
|
|
|
page.off('dialog', dialogHandler)
|
|
})
|
|
}) |