'file']); }); test('the profile form keeps the Email field clear of the Name label above it', function () { $user = User::factory()->create(); $this->actingAs($user); $page = ready(visit('/settings/profile')->resize(1280, 800)); // Livewire fills the fields' values a beat after first paint, and the label's float is a CSS // transition off that: wait it out, or the label is still measured at its unfloated rest position. $page->wait(1); $gap = $page->script("(() => { const fields = document.querySelectorAll('[data-md-input]'); const nameBox = fields[0].querySelector('[data-md-field-box]').getBoundingClientRect(); const emailLabel = fields[1].querySelector('[data-md-field-label]').getBoundingClientRect(); return emailLabel.top - nameBox.bottom; })()"); // The Email field's floated label sits above its own box; it must clear the Name field's box // above it rather than overlap it (M3's 16px form gap is exactly what makes room for this). expect($gap)->toBeGreaterThan(-0.5); }); test('two-factor setup draws a scannable QR code and a visible manual key in dark theme', function () { $user = User::factory()->create(); $this->actingAs($user)->withSession(['auth.password_confirmed_at' => time()]); $page = ready(visit('/settings/two-factor')->inDarkMode()->resize(1280, 800)); $page->click('button:has-text("Enable 2FA")'); $page->wait(1); $metrics = $page->script("(() => { const remPx = parseFloat(getComputedStyle(document.documentElement).fontSize); const box = document.querySelector('.settings-two-factor-qr').getBoundingClientRect(); const rect = document.querySelector('.settings-two-factor-qr svg rect'); const keyInput = document.querySelector('dialog[open] input[readonly]'); const keyBox = keyInput ? keyInput.getBoundingClientRect() : null; return { width: box.width, height: box.height, rem: remPx, fill: rect ? rect.getAttribute('fill') : null, keyVisible: !!keyInput && !!keyBox && keyBox.width > 0 && getComputedStyle(keyInput).visibility !== 'hidden', keyFilled: !!keyInput && keyInput.value.length > 0, }; })()"); expect($metrics['width'])->toEqualWithDelta(16 * $metrics['rem'], 1); expect($metrics['height'])->toEqualWithDelta(16 * $metrics['rem'], 1); expect(strtolower((string) $metrics['fill']))->toBe('#ffffff'); expect($metrics['keyVisible'])->toBeTrue(); expect($metrics['keyFilled'])->toBeTrue(); }); test('the admin dashboard keeps its stats two by two and never scrolls sideways on a phone', function () { $admin = User::factory()->admin()->create(); Share::factory()->count(3)->create(); $this->actingAs($admin); $columns = fn (int $width) => ready(visit('/admin/dashboard')->resize($width, 900))->script("(() => { const rects = [...document.querySelectorAll('[data-md-stat]')].map((el) => el.getBoundingClientRect()); return { rows: new Set(rects.map((r) => Math.round(r.top))).size, cols: new Set(rects.map((r) => Math.round(r.left))).size, }; })()"); $narrow = $columns(839); expect($narrow['rows'])->toBe(2); expect($narrow['cols'])->toBe(2); $wide = $columns(1600); expect($wide['rows'])->toBe(2); expect($wide['cols'])->toBe(2); // The shares are a list that wraps within the page's column, so nothing scrolls sideways. $phone = ready(visit('/admin/dashboard')->resize(393, 852)); $overflow = $phone->script("(() => ({ rows: document.querySelectorAll('[data-test=\"share-row\"]').length, pageScrollWidth: document.documentElement.scrollWidth, windowWidth: window.innerWidth, }))()"); expect($overflow['rows'])->toBe(3); expect($overflow['pageScrollWidth'])->toBeLessThanOrEqual($overflow['windowWidth']); }); test('deleting the account opens its dialog onto a reachable password field, and Escape closes it', function () { $user = User::factory()->create(); $this->actingAs($user); $page = ready(visit('/settings/profile')->resize(1280, 800)); $page->click('[data-test=delete-user-button]'); $page->wait(0.5); $state = $page->script("(() => { const dialog = document.querySelector('dialog[open]'); const input = dialog ? dialog.querySelector('input[type=password]') : null; return { open: !!dialog, reachable: !!input && (document.activeElement === input || (input.tabIndex !== -1 && !input.disabled)), }; })()"); expect($state['open'])->toBeTrue(); expect($state['reachable'])->toBeTrue(); $page->keys('dialog[open] input[type=password]', 'Escape'); $page->wait(0.5); $page->assertScript("document.querySelector('dialog[open]') === null"); });