Files
SealShare/tests/Browser/SealShareTest.php
T
Andreas Reinhold / reiniandClaude Opus 5 21bea9646d
linter / quality (push) Successful in 1m3s
tests / ci (8.5) (push) Successful in 2m8s
docker / test (8.5) (push) Successful in 2m15s
docker / build-and-push (push) Failing after 7m22s
docker / release (push) Skipped
Let the admin choose one of eight colour profiles
Indigo (the default), Blue, Teal, Green, Amber, Rose and Violet in the
Vibrant style and Graphite in the Neutral style are generated from
config into the stylesheet. Admin settings opens with a colour profile
card: a swatch previews the profile on the page, and Save Settings
stores it as color_profile, which AppServiceProvider hands to the
package's resolver, so every page, mail and error page wears it. An
unknown profile is refused, and a saved one that disappears falls back
to indigo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
2026-09-13 15:06:47 +02:00

179 lines
8.7 KiB
PHP

<?php
use App\Models\Setting;
use App\Models\Share;
use App\Models\User;
use App\Services\ShareService;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Storage;
use NoNameWeb\LivewireMaterial\Support\Scheme;
/**
* A page of SealShare, once it can be used: loaded, with Alpine and Livewire started.
*/
function ready(mixed $page): mixed
{
return $page->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'");
});
test('an admin previews a colour profile, saves it, and every page wears it', function () {
$this->actingAs(User::factory()->admin()->create());
$page = ready(visit('/admin/settings'));
$page->assertScript("document.documentElement.getAttribute('data-scheme') === 'indigo'")
->click('[data-test="color-profile"] [data-scheme-option="teal"]')
->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'");
expect(Setting::get('color_profile'))->toBeNull();
$page->click('[data-test="save-settings"]')
->assertSee('Settings saved successfully.');
expect(Setting::get('color_profile'))->toBe('teal');
ready(visit('/upload'))
->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'")
->assertScript("getComputedStyle(document.documentElement).getPropertyValue('--md-sys-color-primary').trim() === '".Scheme::profiles()['teal']['light']['primary']."'")
->assertNoJavaScriptErrors();
});