diff --git a/phpunit.xml b/phpunit.xml
index d703241..602e5cf 100644
--- a/phpunit.xml
+++ b/phpunit.xml
@@ -11,6 +11,9 @@
tests/Feature
+
+ tests/Browser
+
diff --git a/tests/Browser/SealShareTest.php b/tests/Browser/SealShareTest.php
new file mode 100644
index 0000000..d8c7801
--- /dev/null
+++ b/tests/Browser/SealShareTest.php
@@ -0,0 +1,109 @@
+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 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'");
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index a374720..1699180 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -21,7 +21,7 @@ pest()->extend(TestCase::class)
// EnsureSetupComplete middleware redirects to /setup unless an admin exists.
User::factory()->admin()->create(['email' => 'admin-setup@test.com']);
})
- ->in('Feature');
+ ->in('Feature', 'Browser');
/*
|--------------------------------------------------------------------------