waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } beforeEach(function () { // Sessions have to outlive a request here: a sign-in, a verified share password. config(['session.driver' => 'file']); Storage::fake('shares'); }); test('files dragged over the drop zone turn its shape into a burst', function () { $page = ready(visit('/upload')); $burst = "getComputedStyle(document.querySelectorAll('[data-test=drop-zone] span.absolute')[1]).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'") ->assertNoJavaScriptErrors(); }); // Pest's in-process server does not store a multipart upload, so the upload itself is covered by // FileUploadTest; this picks up where it ends, on the page the upload leads to. test('a new share\'s link can be copied from the page the upload leads to', function () { $share = app(ShareService::class)->createShare( [['file' => UploadedFile::fake()->create('contract.pdf', 80), 'relativePath' => null]], [], ); $page = ready(visit(route('share.created', $share, false))); $page->assertSee('Share Created!') ->assertScript("document.querySelector('[data-test=\"share-link\"]').value.includes('/s/')"); $page->script("window.eval(\"Object.defineProperty(navigator, 'clipboard', { configurable: true, value: { writeText: async (text) => { window.copied = text } } })\")"); $page->click('[data-field-copy]') ->assertScript("typeof window.copied === 'string' && window.copied.includes('/s/')") ->assertSee('Copied to the clipboard'); }); test('a new share\'s QR code opens in a dialog and saves as a PNG', function () { $share = Share::factory()->withPassword()->create(); $page = ready(visit(route('share.created', $share, false))); $page->click('[data-test="show-qr-code"]') ->assertScript("document.querySelector('[data-test=\"qr-code-dialog\"]').open") ->assertScript("getComputedStyle(document.querySelector('[data-qr-code]')).backgroundColor === 'rgb(255, 255, 255)'") ->assertScript("document.querySelector('[data-qr-code] svg').getBoundingClientRect().width > 200") ->assertSee('Recipients also need the password.'); // Record what would be saved instead of saving it. $page->script('window.eval("URL.revokeObjectURL = () => {}; HTMLAnchorElement.prototype.click = function () { window.saved = { name: this.download, href: this.href } }")'); $page->click('[data-test="download-qr-code"]') ->assertScript("window.eval('window.saved?.name') === 'share-{$share->token}.png'"); // A QR code is roughly a third to a half dark; a blank or failed drawing is not. $page->script("window.eval(\"(async () => { const blob = await (await fetch(window.saved.href)).blob(); const bitmap = await createImageBitmap(blob); const canvas = new OffscreenCanvas(bitmap.width, bitmap.height); const context = canvas.getContext('2d'); context.drawImage(bitmap, 0, 0); const pixels = context.getImageData(0, 0, bitmap.width, bitmap.height).data; let dark = 0; for (let i = 0; i < pixels.length; i += 4) { if (pixels[i] < 128) { dark++ } } window.png = { type: blob.type, width: bitmap.width, dark: dark / (pixels.length / 4) } })()\")"); $page->assertScript("window.eval('window.png?.type') === 'image/png'") ->assertScript("window.eval('window.png.width') === 1024") ->assertScript("window.eval('window.png.dark') > 0.2 && window.eval('window.png.dark') < 0.6") ->assertNoJavaScriptErrors(); }); test('the share sheet gets the link, and says so only when it fails for another reason than a cancel', function () { $share = Share::factory()->create(); $actions = "Alpine.\$data(document.querySelector('[data-test=share-actions]'))"; $page = ready(visit(route('share.created', $share, false))); // Shown only where the browser has a share sheet. $page->assertScript("window.eval(\"getComputedStyle(document.querySelector('[data-test=share-sheet]').parentElement).display === 'none'\") === (typeof navigator.share !== 'function')"); $page->script("window.eval(\"navigator.share = async (data) => { window.shared = data }; {$actions}.share()\")"); $page->assertScript("window.eval('window.shared?.url') === '".route('share.download', $share)."'"); $page->script("window.eval(\"navigator.share = async () => { throw new DOMException('Cancelled', 'AbortError') }; {$actions}.share()\")"); $page->wait(0.3)->assertDontSee('The share sheet could not open.'); $page->script("window.eval(\"navigator.share = async () => { throw new DOMException('Not allowed', 'NotAllowedError') }; {$actions}.share()\")"); $page->assertSee('The share sheet could not open.'); }); test('a recipient on a phone unlocks a password-protected share and sees its files', function () { $share = app(ShareService::class)->createShare( [['file' => UploadedFile::fake()->create('holiday-photos.zip', 120), 'relativePath' => null]], ['password' => 'correct horse'], ); $page = ready(visit(route('share.download', $share, false))->resize(393, 852)); $page->assertSee('Password Required') ->assertScript('document.documentElement.scrollWidth <= window.innerWidth') ->assertNoJavaScriptErrors() ->type('input[type="password"]', 'correct horse') ->press('Unlock') ->assertSee('Shared Files') ->assertSee('holiday-photos.zip') ->assertScript("document.querySelectorAll('[popover]').length === 0") ->assertScript('document.documentElement.scrollWidth <= window.innerWidth'); }); test('an admin sorts the shares table and deletes a share through its dialog', function () { $admin = User::factory()->admin()->create(); Share::factory()->create(['token' => 'aaaaaaaaaaaaaaaa', 'download_count' => 9]); $doomed = Share::factory()->create(['token' => 'zzzzzzzzzzzzzzzz', 'download_count' => 1]); $this->actingAs($admin); $page = ready(visit('/admin/dashboard')); $page->click('th button:has-text("Downloads")') ->assertScript("document.querySelector('tbody tr td').textContent.trim() === 'zzzzzzzzzzzzzzzz'"); $page->click("[data-test=\"delete-share-{$doomed->id}\"]") ->assertScript("[...document.querySelectorAll('dialog')].some((dialog) => dialog.open)") ->click('[data-test="confirm-delete-share"]') ->assertScript("! [...document.querySelectorAll('dialog')].some((dialog) => dialog.open)") ->assertDontSee('zzzzzzzzzzzzzzzz'); expect(Share::query()->find($doomed->id))->toBeNull(); }); test('a first visit follows the system theme, and Appearance switches it', function () { ready(visit('/upload')->inDarkMode()) ->assertScript("document.documentElement.dataset.theme === 'dark'") ->assertScript("document.documentElement.dataset.themeChoice === 'system'"); $this->actingAs(User::factory()->create()); $page = ready(visit('/settings/appearance')->inDarkMode()); $page->click('[data-theme-option="light"]') ->assertScript("document.documentElement.dataset.theme === 'light'") ->assertScript("localStorage.getItem('sealshare-theme') === 'light'"); });