Offer a new share as a QR code and through the share sheet
The share created page gets "Show QR code", a dialog with the link as a QR code (black on white, full screen on a phone) that downloads as a PNG drawn in the browser, with a password reminder for protected shares; and "Share…", which opens the device's share sheet where there is one. Both carry only the link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V9NnLxnPp8vaaurb3Z1MFy
This commit is contained in:
co-authored by
Claude Opus 5
parent
fbe358b2e5
commit
1352533667
@@ -54,6 +54,51 @@ test('a new share\'s link can be copied from the page the upload leads to', func
|
||||
->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]],
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Share;
|
||||
use App\Services\QrCodeService;
|
||||
|
||||
test('the share created page offers the link as a QR code and through the share sheet', function () {
|
||||
$share = Share::factory()->create();
|
||||
$url = route('share.download', $share);
|
||||
|
||||
$response = $this->get(route('share.created', $share));
|
||||
|
||||
$response->assertOk()
|
||||
->assertSee($url, false)
|
||||
->assertSee('data-test="show-qr-code"', false)
|
||||
->assertSee('data-test="share-sheet"', false)
|
||||
->assertSee('share-'.$share->token.'.png')
|
||||
->assertSee('<div data-qr-code class="mx-auto aspect-square w-full max-w-80 rounded-corner-lg bg-white p-2 [&>svg]:size-full">'.app(QrCodeService::class)->svg($url).'</div>', false)
|
||||
->assertDontSee('Recipients also need the password.');
|
||||
});
|
||||
|
||||
test('the QR code dialog reminds that a protected share also needs its password', function () {
|
||||
$share = Share::factory()->withPassword()->create();
|
||||
|
||||
$this->get(route('share.created', $share))
|
||||
->assertOk()
|
||||
->assertSee('Recipients also need the password.');
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
use App\Services\QrCodeService;
|
||||
|
||||
test('it draws a 1024 pixel QR code as inline SVG, black on white', function () {
|
||||
$svg = (new QrCodeService)->svg('https://share.example.com/s/aBcDeFgHiJkLmNoP');
|
||||
|
||||
expect($svg)->toStartWith('<svg ')
|
||||
->not->toContain('<?xml')
|
||||
->toContain('width="1024" height="1024"')
|
||||
->toContain('fill="#ffffff"')
|
||||
->toContain('fill="#000000"');
|
||||
});
|
||||
|
||||
test('the same contents give the same code, other contents another', function () {
|
||||
$service = new QrCodeService;
|
||||
|
||||
expect($service->svg('https://share.example.com/s/aBcDeFgHiJkLmNoP'))
|
||||
->toBe($service->svg('https://share.example.com/s/aBcDeFgHiJkLmNoP'))
|
||||
->not->toBe($service->svg('https://share.example.com/s/zYxWvUtSrQpOnMlK'));
|
||||
});
|
||||
Reference in New Issue
Block a user