'file']); Storage::fake('shares'); }); test('tab reaches Browse Files with its focus ring, and Enter or Space opens the file picker', function () { $page = ready(visit('/upload')); // A spy, not a real dialog: Playwright would otherwise have to field a native file chooser. $page->script("window.eval(\"window.__fileInputClicks = 0; HTMLInputElement.prototype.click = function () { if (this.type === 'file') { window.__fileInputClicks++ } }\")"); // "body" alone is not a CSS-explicit selector to this plugin's guesser (no special chars) and // falls back to a text search, which never matches and times out; "html > body" is explicit and // focuses nothing new (the body takes no tabindex), so the Tab lands where a fresh page load // would send it: the first tabbable element. $page->keys('html > body', 'Tab'); $page->assertScript("document.activeElement.matches('[data-md-button]') && document.activeElement.textContent.trim() === 'Browse Files'"); $ring = $page->script('getComputedStyle(document.activeElement).outlineStyle'); expect($ring)->toBe('solid'); $page->keys(':focus', 'Enter'); $page->assertScript('window.__fileInputClicks === 1'); $page->keys(':focus', 'Space'); $page->assertScript('window.__fileInputClicks === 2') ->assertNoJavaScriptErrors(); }); test('the selected files list avoids horizontal overflow once files are chosen', function () { // Pest's in-process browser server never parses the multipart body a real file selection sends // (vendor/pestphp/pest-plugin-browser/src/Drivers/LaravelHttpServer.php:257, `[], // @TODO // files...`) — Livewire's temporary-upload request has nowhere to land, so a selected file can // never reach this list to be measured. tests/Feature/FileUploadTest.php covers the list's data // through Livewire::test(); driving a real selection through the browser is impractical here. })->skip('the in-process browser server drops multipart uploads (LaravelHttpServer.php:257): a real file selection cannot reach the list'); test('the drop zone hides its burst again once the drag leaves', function () { $page = ready(visit('/upload')); $burst = "getComputedStyle(document.querySelector('[data-test=drop-zone-burst]')).opacity"; $page->assertScript("{$burst} === '0'"); $page->script("window.eval(\"document.querySelector('[data-test=drop-zone]').dispatchEvent(new DragEvent('dragover', { bubbles: true, cancelable: true }))\")"); $page->assertScript("{$burst} === '1'"); $page->script("window.eval(\"document.querySelector('[data-test=drop-zone]').dispatchEvent(new DragEvent('dragleave', { bubbles: true, cancelable: true }))\")"); $page->assertScript("{$burst} === '0'") ->assertNoJavaScriptErrors(); }); test('the QR dialog holds its code inside the box, and Escape returns focus to the button that opened it', function () { $share = Share::factory()->create(); $page = ready(visit(route('share.created', $share, false))); $page->click('[data-test="show-qr-code"]'); $page->wait(1); $page->assertScript("document.querySelector('[data-test=\"qr-code-dialog\"]').open"); $inside = $page->script("(() => { const dialog = document.querySelector('[data-test=\"qr-code-dialog\"]').getBoundingClientRect(); const qr = document.querySelector('[data-qr-code] svg').getBoundingClientRect(); return qr.left >= dialog.left - 1 && qr.right <= dialog.right + 1 && qr.top >= dialog.top - 1 && qr.bottom <= dialog.bottom + 1; })()"); expect($inside)->toBeTrue(); $page->keys(':focus', 'Escape'); $page->wait(1); $page->assertScript("! document.querySelector('[data-test=\"qr-code-dialog\"]').open") ->assertScript("document.activeElement === document.querySelector('[data-test=\"show-qr-code\"]')") ->assertNoJavaScriptErrors(); }); test('the download page fits a phone before and after unlocking a password-protected share', function () { $share = app(ShareService::class)->createShare( [['file' => UploadedFile::fake()->create('a-genuinely-quite-long-holiday-photos-archive-from-portugal.zip', 120), 'relativePath' => null]], ['password' => 'let-me-in'], ); $page = ready(visit(route('share.download', $share, false))->resize(393, 852)); $metrics = $page->script("(() => { const button = document.querySelector('button[type=submit]'); const form = button.closest('[data-md-form]').getBoundingClientRect(); const rect = button.getBoundingClientRect(); return { formWidth: form.width, formRight: form.right, buttonWidth: rect.width, buttonRight: rect.right }; })()"); expect($metrics['buttonWidth'])->toBeLessThan($metrics['formWidth']); expect($metrics['buttonRight'])->toEqualWithDelta($metrics['formRight'], 1); $page->assertScript('document.documentElement.scrollWidth <= window.innerWidth'); $page->type('input[type="password"]', 'let-me-in')->press('Unlock'); $page->wait(1); $page->assertScript("document.querySelector('h2').tagName === 'H2' && document.querySelector('h2').textContent.trim() === 'Shared Files'"); $rows = $page->script("(() => { const rows = [...document.querySelectorAll('[data-md-list-item]')]; return { count: rows.length, allFit: rows.every((row) => row.getBoundingClientRect().right <= window.innerWidth + 0.5), }; })()"); expect($rows['count'])->toBeGreaterThan(0); expect($rows['allFit'])->toBeTrue(); $page->assertScript('document.documentElement.scrollWidth <= window.innerWidth') ->assertNoJavaScriptErrors(); }); test('the settings Save button is end-aligned at less than the form\'s width', function (string $url, string $button) { $this->actingAs($url === '/admin/settings' ? User::factory()->admin()->create() : User::factory()->create()); $page = ready(visit($url)->resize(1280, 800)); $metrics = $page->script("(() => { const button = document.querySelector('{$button}'); const form = button.closest('[data-md-form]').getBoundingClientRect(); const rect = button.getBoundingClientRect(); return { formWidth: form.width, formRight: form.right, buttonWidth: rect.width, buttonRight: rect.right }; })()"); expect($metrics['buttonWidth'])->toBeLessThan($metrics['formWidth']); expect($metrics['buttonRight'])->toEqualWithDelta($metrics['formRight'], 1); })->with([ ['/settings/profile', '[data-test="update-profile-button"]'], ['/admin/settings', '[data-test="save-settings"]'], ]); test('the admin dashboard heads its shares table with an h2 and drops the card', function () { $admin = User::factory()->admin()->create(); Share::factory()->count(2)->create(); $this->actingAs($admin); $page = ready(visit('/admin/dashboard')); $result = $page->script("(() => { const heading = [...document.querySelectorAll('h2')].find((h) => h.textContent.trim() === 'All Shares'); const table = document.querySelector('table'); return { headingIsH2: heading?.tagName === 'H2', headingBeforeTable: !!heading && !!table && !!(heading.compareDocumentPosition(table) & Node.DOCUMENT_POSITION_FOLLOWING), noCard: document.querySelectorAll('[data-md-card]').length === 0, }; })()"); expect($result['headingIsH2'])->toBeTrue(); expect($result['headingBeforeTable'])->toBeTrue(); expect($result['noCard'])->toBeTrue(); }); test('the admin settings page has five headed sections and no card', function () { $this->actingAs(User::factory()->admin()->create()); $page = ready(visit('/admin/settings')); $result = $page->script("(() => { return { // Every dialog's own title is an h2 too (components/modal.blade.php), whether open or // not: exclude those to count only the page's own section headings. h2Count: document.querySelectorAll('h2:not([data-md-modal-title])').length, noCard: document.querySelectorAll('[data-md-card]').length === 0, }; })()"); expect($result['h2Count'])->toBe(5); expect($result['noCard'])->toBeTrue(); });