optimizations

This commit is contained in:
2026-03-28 19:15:39 +01:00
parent 0b25d9570a
commit cab4fbd83e
509 changed files with 1016804 additions and 1605 deletions

View File

@@ -0,0 +1,230 @@
import { test, expect, type Page } from '@playwright/test'
import { execFileSync } from 'node:child_process'
import { existsSync, statSync } from 'node:fs'
import path from 'node:path'
type NovaCardFixture = {
email: string
password: string
username: string
}
const VITE_MANIFEST_PATH = path.join(process.cwd(), 'public', 'build', 'manifest.json')
const FRONTEND_SOURCES = [
path.join(process.cwd(), 'resources', 'js', 'Pages', 'Studio', 'StudioCardEditor.jsx'),
path.join(process.cwd(), 'resources', 'js', 'components', 'nova-cards', 'NovaCardCanvasPreview.jsx'),
]
function needsFrontendBuild() {
if (!existsSync(VITE_MANIFEST_PATH)) {
return true
}
const manifestUpdatedAt = statSync(VITE_MANIFEST_PATH).mtimeMs
return FRONTEND_SOURCES.some((filePath) => existsSync(filePath) && statSync(filePath).mtimeMs > manifestUpdatedAt)
}
function ensureCompiledAssets() {
if (!needsFrontendBuild()) {
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 seedNovaCardFixture(): NovaCardFixture {
const token = `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}`
const email = `e2e-nova-cards-${token}@example.test`
const username = `e2enc${token}`.slice(0, 20)
const script = [
'use App\\Models\\User;',
'use Illuminate\\Support\\Facades\\Hash;',
`$user = User::updateOrCreate(['email' => '${email}'], [`,
" 'name' => 'E2E Nova Cards User',",
` 'username' => '${username}',`,
" 'onboarding_step' => 'complete',",
" 'email_verified_at' => now(),",
" 'is_active' => 1,",
" 'password' => Hash::make('password'),",
']);',
"echo json_encode(['email' => $user->email, 'password' => 'password', 'username' => $user->username]);",
].join(' ')
const raw = execFileSync('php', ['artisan', 'tinker', `--execute=${script}`], {
cwd: process.cwd(),
encoding: 'utf8',
})
const lines = raw
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean)
const jsonLine = [...lines].reverse().find((line) => line.startsWith('{') && line.endsWith('}'))
if (!jsonLine) {
throw new Error(`Unable to parse fixture JSON from tinker output: ${raw}`)
}
return JSON.parse(jsonLine) as NovaCardFixture
}
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',
})
}
async function login(page: Page, fixture: NovaCardFixture) {
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 mobile editor login failed because the login page returned an internal server error.')
}
await emailField.fill(fixture.email)
await page.locator('input[name="password"]').fill(fixture.password)
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 Cards User/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 mobile editor login failed before reaching an authenticated page.')
}
}
}
async function expectNoHorizontalOverflow(page: Page) {
const dimensions = await page.evaluate(() => ({
clientWidth: document.documentElement.clientWidth,
scrollWidth: document.documentElement.scrollWidth,
}))
expect(
dimensions.scrollWidth,
`Expected no horizontal overflow, but scrollWidth=${dimensions.scrollWidth} and clientWidth=${dimensions.clientWidth}`
).toBeLessThanOrEqual(dimensions.clientWidth + 1)
}
function fieldInsideLabel(page: Page, labelText: string, element: 'input' | 'textarea' | 'select' = 'input') {
return page.locator('label', { hasText: labelText }).locator(element).first()
}
test.describe('Nova Cards mobile editor', () => {
test.describe.configure({ mode: 'serial' })
test.use({ viewport: { width: 390, height: 844 } })
let fixture: NovaCardFixture
test.beforeAll(() => {
ensureCompiledAssets()
ensureNovaCardSeedData()
fixture = seedNovaCardFixture()
})
test.beforeEach(() => {
resetBotProtectionState()
})
test('step navigation exposes the mobile editor flow', async ({ page }) => {
await login(page, fixture)
await page.goto('/studio/cards/create', { waitUntil: 'domcontentloaded' })
await expect(page.getByRole('heading', { name: 'Structured card creation with live preview and autosave.' })).toBeVisible()
await expect(page.getByRole('button', { name: 'Save' })).toBeEnabled({ timeout: 15000 })
await expect(page.getByText('Step 1 / 6')).toBeVisible()
await expect(page.getByText('Choose the canvas shape and basic direction.')).toBeVisible()
await expect(page.getByRole('combobox', { name: 'Format' })).toBeVisible()
await expect(fieldInsideLabel(page, 'Quote text', 'textarea')).toBeHidden()
await expectNoHorizontalOverflow(page)
await page.getByRole('button', { name: 'Next', exact: true }).click()
await expect(page.getByText('Step 2 / 6')).toBeVisible()
await expect(page.getByText('Pick the visual foundation for the card.')).toBeVisible()
await expect(page.getByRole('combobox', { name: 'Overlay' })).toBeVisible()
await expect(page.getByRole('combobox', { name: 'Focal position' })).toBeVisible()
await page.getByRole('button', { name: 'Next', exact: true }).click()
await expect(page.getByText('Step 3 / 6')).toBeVisible()
await expect(fieldInsideLabel(page, 'Quote text', 'textarea')).toBeVisible()
await fieldInsideLabel(page, 'Title').fill('Mobile editor flow card')
await fieldInsideLabel(page, 'Quote text', 'textarea').fill('Mobile step preview quote')
await fieldInsideLabel(page, 'Author').fill('Playwright')
await page.getByRole('button', { name: 'Next', exact: true }).click()
await expect(page.getByText('Step 4 / 6')).toBeVisible()
await expect(page.getByRole('combobox', { name: 'Text width' })).toBeVisible()
await expect(page.getByRole('combobox', { name: 'Line height' })).toBeVisible()
await page.getByRole('button', { name: 'Next', exact: true }).click()
await expect(page.getByText('Step 5 / 6')).toBeVisible()
await expect(page.getByText('Check the live composition before publish.')).toBeVisible()
await expect(page.getByText('Mobile step preview quote').first()).toBeVisible()
await expect(page.getByText('Playwright').first()).toBeVisible()
await page.getByRole('button', { name: 'Next', exact: true }).click()
await expect(page.getByText('Step 6 / 6')).toBeVisible()
await expect(page.getByRole('combobox', { name: 'Visibility' })).toBeVisible()
await expect(page.getByText('Draft actions')).toBeVisible()
await expect(fieldInsideLabel(page, 'Quote text', 'textarea')).toBeHidden()
await page.getByRole('button', { name: 'Back', exact: true }).click()
await expect(page.getByText('Step 5 / 6')).toBeVisible()
await expect(page.getByText('Mobile step preview quote').first()).toBeVisible()
await expectNoHorizontalOverflow(page)
})
})