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 DashboardFixture = { email: string password: string username: string } const VITE_MANIFEST_PATH = path.join(process.cwd(), 'public', 'build', 'manifest.json') function ensureCompiledAssets() { if (existsSync(VITE_MANIFEST_PATH)) { return } const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm' execFileSync(npmCommand, ['run', 'build'], { cwd: process.cwd(), stdio: 'inherit', }) } function seedDashboardFixture(): DashboardFixture { const token = `${Date.now().toString().slice(-6)}${Math.floor(Math.random() * 1000).toString().padStart(3, '0')}` const email = `e2e-dashboard-mobile-${token}@example.test` const username = `e2em${token}`.slice(0, 20) const script = [ 'use App\\Models\\User;', 'use Illuminate\\Support\\Facades\\Hash;', `$user = User::updateOrCreate(['email' => '${email}'], [`, " 'name' => 'E2E Mobile Dashboard 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 DashboardFixture } 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: DashboardFixture) { 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('Dashboard mobile layout 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 Mobile Dashboard 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('Dashboard mobile layout 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) } test.describe('Dashboard mobile layout', () => { test.describe.configure({ mode: 'serial' }) test.use({ viewport: { width: 390, height: 844 } }) let fixture: DashboardFixture test.beforeAll(() => { ensureCompiledAssets() fixture = seedDashboardFixture() }) test.beforeEach(() => { resetBotProtectionState() }) test('dashboard home fits mobile width', async ({ page }) => { await login(page, fixture) await page.goto('/dashboard', { waitUntil: 'domcontentloaded' }) await expect(page.getByRole('heading', { name: /welcome back/i })).toBeVisible() await page.waitForLoadState('networkidle') await expectNoHorizontalOverflow(page) await expect(page.getByRole('heading', { name: 'Your dashboard snapshot' })).toBeVisible() }) test('followers page fits mobile width', async ({ page }) => { await login(page, fixture) await page.goto('/dashboard/followers', { waitUntil: 'domcontentloaded' }) await expect(page.getByRole('heading', { name: 'People Following Me' })).toBeVisible() await page.waitForLoadState('networkidle') await expectNoHorizontalOverflow(page) await expect(page.getByRole('link', { name: /discover creators/i })).toBeVisible() }) test('following page fits mobile width', async ({ page }) => { await login(page, fixture) await page.goto('/dashboard/following', { waitUntil: 'domcontentloaded' }) await expect(page.getByRole('heading', { name: 'People I Follow' })).toBeVisible() await page.waitForLoadState('networkidle') await expectNoHorizontalOverflow(page) await expect(page.getByRole('link', { name: /my followers/i })).toBeVisible() }) })