Give every page the share pages' layout, at one width
The pages were built five ways: two layouts, seven widths from 28 to 64rem and four heading styles. Every page now looks like the share pages: a centred heading over one 40rem column of outlined cards, with the floating toolbar below. - <x-page> (components/page.blade.php) is the root of every page. It draws the h1 and its line (`brand` takes the site's logo, title and description from Admin settings), an optional `mark` and `navigation` slot, then the content. It has no width prop: every page is the same <x-pane width="narrow">. - The sign-in, password reset, confirm, verify email, two-factor challenge, setup and system password pages move onto layouts/app with the brand heading and their form in a card titled with the task. layouts/auth, auth-header and the settings heading partial are gone, and so is the per-page width CSS. - Settings put their section nav under the heading; the admin pages get a description line each. FileUploader and ShareDownload no longer pass the branding to their views. - The admin dashboard's table needed about 49rem, so its shares are a list: created above the token, which opens the share, then files, size, downloads and expiry on two lines that wrap instead of clipping, and one delete button. A "Sort by" select replaces the column headers (newest, oldest, expiring soonest with never-expiring last, largest, most downloads, most files) and resets the page. The stats stay two by two. table.css and sort-header.css are no longer imported. - Branding hints in Admin settings name every page the title shows on. - Tests: PageTemplateTest renders every page once and checks one page template, one h1 and the width, and the brand heading with its fallbacks. FrameTest measures the page column instead of the auth card and the 64rem main; dashboard tests follow the list and the sort select, including expiry order. .ai/rules/views.md records <x-page>, the CHANGELOG notes the change and the website screenshots are regenerated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
40e35bab0e
commit
5853f1f7d6
@@ -11,7 +11,7 @@ use Illuminate\Support\Facades\Storage;
|
||||
* The walk: every page's chrome and content across M3's breakpoint edges — 599, 600, 839, 840,
|
||||
* 1199, 1200, 1600px, height 900, light theme — one visit per page, reused across widths by
|
||||
* resizing the same page rather than revisiting it. FrameTest.php and SettingsAndAdminTest.php
|
||||
* already assert the app-main margin, the auth card's alignment and the stat grid's column count
|
||||
* already assert the app-main margin, the page column's width and the stat grid's column count
|
||||
* at their own boundary for the pages they cover; this file re-asserts those three plus the
|
||||
* section navigation's picker/tab-bar switch (untested until now), and walks every page group A/B/C
|
||||
* left unchecked at a breakpoint: the rest of the auth flow, the setup wizard, the system password
|
||||
@@ -243,9 +243,9 @@ test('the admin dashboard holds at every breakpoint', function () {
|
||||
}
|
||||
});
|
||||
|
||||
// <x-grid :columns="['compact' => 2, 'expanded' => 4]">: 2 columns below `expanded` (840px), 4 from it.
|
||||
// <x-grid :columns="2">: two columns at every width, since the page's column is 40rem at all of them.
|
||||
expect($columns[839])->toBe(2);
|
||||
expect($columns[840])->toBe(4);
|
||||
expect($columns[840])->toBe(2);
|
||||
});
|
||||
|
||||
test('the admin settings page holds at every breakpoint', function () {
|
||||
|
||||
+29
-64
@@ -1,12 +1,12 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
/**
|
||||
* Group A's frame: the auth layout's centred card and the app layout's main column
|
||||
* (resources/views/layouts/*, partials/*, components/*), regardless of which page renders inside
|
||||
* them — a page's own content may still be unstyled (a later batch), but the chrome around it is
|
||||
* group A's and must hold its geometry.
|
||||
* The frame every page shares: the app layout's main region (resources/views/layouts/app.blade.php,
|
||||
* partials/toolbar.blade.php) and the page template's centred column inside it
|
||||
* (resources/views/components/page.blade.php), whichever page renders there.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
config(['session.driver' => 'file']);
|
||||
@@ -21,50 +21,33 @@ test('the sign-in page has exactly one main landmark and never scrolls sideways'
|
||||
}
|
||||
});
|
||||
|
||||
test('the sign-in card is capped at 28rem and sits 16px from each edge on a phone', function () {
|
||||
$page = ready(visit('/login')->resize(393, 852));
|
||||
test('every page column is 40rem and centred on a wide window', function (string $url, bool $asAdmin) {
|
||||
if ($asAdmin) {
|
||||
$this->actingAs(User::factory()->admin()->create());
|
||||
}
|
||||
|
||||
$metrics = $page->script("(() => {
|
||||
const rect = document.querySelector('.auth-card').getBoundingClientRect();
|
||||
return { left: rect.left, right: window.innerWidth - rect.right, width: rect.width };
|
||||
$metrics = ready(visit($url)->resize(1600, 900))->script("(() => {
|
||||
const remPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
const rect = document.querySelector('[data-test=\"page\"]').getBoundingClientRect();
|
||||
return { width: rect.width, remPx, left: rect.left, right: window.innerWidth - rect.right };
|
||||
})()");
|
||||
|
||||
expect($metrics['width'])->toEqualWithDelta(40 * $metrics['remPx'], 0.5);
|
||||
expect($metrics['left'])->toEqualWithDelta($metrics['right'], 1);
|
||||
})->with([
|
||||
'sign-in' => ['/login', false],
|
||||
'upload' => ['/upload', false],
|
||||
'admin dashboard' => ['/admin/dashboard', true],
|
||||
]);
|
||||
|
||||
test('the page column sits 16px from each edge on a phone', function () {
|
||||
$metrics = ready(visit('/login')->resize(393, 852))->script("(() => {
|
||||
const rect = document.querySelector('[data-test=\"page\"]').getBoundingClientRect();
|
||||
return { left: rect.left, right: window.innerWidth - rect.right };
|
||||
})()");
|
||||
|
||||
expect($metrics['left'])->toEqualWithDelta(16, 1);
|
||||
expect($metrics['right'])->toEqualWithDelta(16, 1);
|
||||
|
||||
$page->resize(1280, 800);
|
||||
|
||||
$capped = $page->script("(() => {
|
||||
const remPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
return document.querySelector('.auth-card').getBoundingClientRect().width <= 28 * remPx + 0.5;
|
||||
})()");
|
||||
|
||||
expect($capped)->toBeTrue();
|
||||
});
|
||||
|
||||
test('the sign-in card top-aligns below 600px and centres from 600px', function () {
|
||||
// The gap above the card and the gap below it, inside .auth-main's own content box (between
|
||||
// its top and bottom padding) — equal gaps is the geometric definition of "centred", and it
|
||||
// needs no assumption about the padding's pixel values, only that they exist on both edges.
|
||||
$gaps = fn (int $width) => ready(visit('/login')->resize($width, 900))->script("(() => {
|
||||
const main = document.querySelector('.auth-main');
|
||||
const mainStyle = getComputedStyle(main);
|
||||
const mainRect = main.getBoundingClientRect();
|
||||
const cardRect = document.querySelector('.auth-card').getBoundingClientRect();
|
||||
const contentTop = mainRect.top + parseFloat(mainStyle.paddingTop);
|
||||
const contentBottom = mainRect.bottom - parseFloat(mainStyle.paddingBottom);
|
||||
return { above: cardRect.top - contentTop, below: contentBottom - cardRect.bottom };
|
||||
})()");
|
||||
|
||||
$narrow = $gaps(599);
|
||||
// Below 600px the card is flush against the top of the content box: no extra gap above it.
|
||||
expect($narrow['above'])->toBeLessThan(2);
|
||||
expect($narrow['below'])->toBeGreaterThan($narrow['above'] + 10);
|
||||
|
||||
$wide = $gaps(600);
|
||||
// From 600px the gap above equals the gap below: the card is vertically centred.
|
||||
expect($wide['above'])->toEqualWithDelta($wide['below'], 2);
|
||||
expect($wide['above'])->toBeGreaterThan($narrow['above'] + 10);
|
||||
});
|
||||
|
||||
test('the sign-in submit button is end-aligned at its own width, not stretched', function () {
|
||||
@@ -86,7 +69,7 @@ test('the floating toolbar never covers the sign-in card once scrolled to the bo
|
||||
$page->script('window.scrollTo(0, document.body.scrollHeight)');
|
||||
|
||||
$overlap = $page->script("(() => {
|
||||
const card = document.querySelector('.auth-card').getBoundingClientRect();
|
||||
const card = document.querySelector('[data-test=\"page\"] [data-md-card]').getBoundingClientRect();
|
||||
const toolbar = document.querySelector('[data-test=\"app-toolbar\"]').getBoundingClientRect();
|
||||
return card.bottom - toolbar.top;
|
||||
})()");
|
||||
@@ -109,27 +92,9 @@ test("a snackbar clears SealShare's floating toolbar", function () {
|
||||
expect($gap)->toBeGreaterThanOrEqual(16 - 0.5);
|
||||
});
|
||||
|
||||
test("the app layout's main column is capped at 64rem and centred on a wide window", function () {
|
||||
$page = ready(visit('/upload')->resize(1600, 900));
|
||||
|
||||
$metrics = $page->script("(() => {
|
||||
const remPx = parseFloat(getComputedStyle(document.documentElement).fontSize);
|
||||
const rect = document.querySelector('.app-main').getBoundingClientRect();
|
||||
return {
|
||||
width: rect.width,
|
||||
cap: 64 * remPx,
|
||||
left: rect.left,
|
||||
right: window.innerWidth - rect.right,
|
||||
};
|
||||
})()");
|
||||
|
||||
expect($metrics['width'])->toBeLessThanOrEqual($metrics['cap'] + 0.5);
|
||||
expect($metrics['left'])->toEqualWithDelta($metrics['right'], 1);
|
||||
});
|
||||
|
||||
test("the app layout's content keeps M3's margin at 599px and 600px", function () {
|
||||
// Below the 64rem cap the pane spans the full window (no centring offset), so the padding
|
||||
// it declares on its body is exactly the gap between its content and the window's edge.
|
||||
// The main region spans the full window at every width (the page inside sets its own width),
|
||||
// so the padding it declares on its body is exactly the gap between its content and the edge.
|
||||
$margins = fn (int $width) => ready(visit('/upload')->resize($width, 900))->script("(() => {
|
||||
const main = document.querySelector('.app-main').getBoundingClientRect();
|
||||
const style = getComputedStyle(document.querySelector('[data-md-pane-body]'));
|
||||
|
||||
@@ -172,17 +172,19 @@ test('a recipient on a phone unlocks a password-protected share and sees its fil
|
||||
->assertScript('document.documentElement.scrollWidth <= window.innerWidth');
|
||||
});
|
||||
|
||||
test('an admin sorts the shares table and deletes a share through its dialog', function () {
|
||||
test('an admin sorts the shares list 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]);
|
||||
Share::factory()->create(['token' => 'aaaaaaaaaaaaaaaa', 'download_count' => 1, 'created_at' => now()->subDay()]);
|
||||
$doomed = Share::factory()->create(['token' => 'zzzzzzzzzzzzzzzz', 'download_count' => 9, 'created_at' => now()->subDays(2)]);
|
||||
|
||||
$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->assertScript("document.querySelector('[data-test=\"share-row\"] code').textContent.trim() === 'aaaaaaaaaaaaaaaa'")
|
||||
->select('[data-test="shares-sort"]', 'most-downloaded')
|
||||
->wait(0.5)
|
||||
->assertScript("document.querySelector('[data-test=\"share-row\"] code').textContent.trim() === 'zzzzzzzzzzzzzzzz'");
|
||||
|
||||
$page->click("[data-test=\"delete-share-{$doomed->id}\"]")
|
||||
->assertScript("[...document.querySelectorAll('dialog')].some((dialog) => dialog.open)")
|
||||
|
||||
@@ -11,9 +11,9 @@ use App\Models\User;
|
||||
*
|
||||
* Out of scope by a later decision (M3's guidance over 1.x's look, reworked in this batch): form
|
||||
* actions' placement/width, the shares table's relation to its card, and the admin settings
|
||||
* cards' grouping — now end-aligned actions, a card-free shares section and headed/divided
|
||||
* settings sections respectively. Nothing here asserts any of those; a browser run follows this
|
||||
* review.
|
||||
* cards' grouping — now end-aligned actions, the shares table in its card and each settings
|
||||
* section in a card of its own respectively (tests/Browser/ShareFlowTest.php asserts the last two).
|
||||
* Nothing here asserts any of those.
|
||||
*/
|
||||
beforeEach(function () {
|
||||
config(['session.driver' => 'file']);
|
||||
@@ -72,7 +72,7 @@ test('two-factor setup draws a scannable QR code and a visible manual key in dar
|
||||
expect($metrics['keyFilled'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('the admin dashboard sizes its stat grid by width and never scrolls sideways on a phone', function () {
|
||||
test('the admin dashboard keeps its stats two by two and never scrolls sideways on a phone', function () {
|
||||
$admin = User::factory()->admin()->create();
|
||||
Share::factory()->count(3)->create();
|
||||
$this->actingAs($admin);
|
||||
@@ -89,24 +89,20 @@ test('the admin dashboard sizes its stat grid by width and never scrolls sideway
|
||||
expect($narrow['rows'])->toBe(2);
|
||||
expect($narrow['cols'])->toBe(2);
|
||||
|
||||
$wide = $columns(840);
|
||||
expect($wide['rows'])->toBe(1);
|
||||
expect($wide['cols'])->toBe(4);
|
||||
$wide = $columns(1600);
|
||||
expect($wide['rows'])->toBe(2);
|
||||
expect($wide['cols'])->toBe(2);
|
||||
|
||||
// The table is wide enough on a phone to need its own horizontal scroll (so this is not a
|
||||
// vacuous check); the page itself must never pick that scroll up.
|
||||
// The shares are a list that wraps within the page's column, so nothing scrolls sideways.
|
||||
$phone = ready(visit('/admin/dashboard')->resize(393, 852));
|
||||
|
||||
$overflow = $phone->script("(() => {
|
||||
const scroller = document.querySelector('.admin-shares-table-scroll');
|
||||
return {
|
||||
tableNeedsScroll: scroller.scrollWidth > scroller.clientWidth + 1,
|
||||
pageScrollWidth: document.documentElement.scrollWidth,
|
||||
windowWidth: window.innerWidth,
|
||||
};
|
||||
})()");
|
||||
$overflow = $phone->script("(() => ({
|
||||
rows: document.querySelectorAll('[data-test=\"share-row\"]').length,
|
||||
pageScrollWidth: document.documentElement.scrollWidth,
|
||||
windowWidth: window.innerWidth,
|
||||
}))()");
|
||||
|
||||
expect($overflow['tableNeedsScroll'])->toBeTrue();
|
||||
expect($overflow['rows'])->toBe(3);
|
||||
expect($overflow['pageScrollWidth'])->toBeLessThanOrEqual($overflow['windowWidth']);
|
||||
});
|
||||
|
||||
|
||||
@@ -175,7 +175,7 @@ test('the settings Save button is end-aligned at less than the form\'s width', f
|
||||
['/admin/settings', '[data-test="save-settings"]'],
|
||||
]);
|
||||
|
||||
test('the admin dashboard heads its shares table with an h2 and drops the card', function () {
|
||||
test('the admin dashboard heads its shares list with an h2, both in one card', function () {
|
||||
$admin = User::factory()->admin()->create();
|
||||
Share::factory()->count(2)->create();
|
||||
$this->actingAs($admin);
|
||||
@@ -184,34 +184,39 @@ test('the admin dashboard heads its shares table with an h2 and drops the card',
|
||||
|
||||
$result = $page->script("(() => {
|
||||
const heading = [...document.querySelectorAll('h2')].find((h) => h.textContent.trim() === 'All Shares');
|
||||
const table = document.querySelector('table');
|
||||
const list = document.querySelector('[data-test=\"share-row\"]');
|
||||
const card = heading?.closest('[data-md-card]');
|
||||
return {
|
||||
headingIsH2: heading?.tagName === 'H2',
|
||||
headingBeforeTable: !!heading && !!table
|
||||
&& !!(heading.compareDocumentPosition(table) & Node.DOCUMENT_POSITION_FOLLOWING),
|
||||
noCard: document.querySelectorAll('[data-md-card]').length === 0,
|
||||
headingIsCardTitle: heading?.matches('[data-md-card-title]') ?? false,
|
||||
listInSameCard: !!card && card.contains(list),
|
||||
headingBeforeList: !!heading && !!list
|
||||
&& !!(heading.compareDocumentPosition(list) & Node.DOCUMENT_POSITION_FOLLOWING),
|
||||
};
|
||||
})()");
|
||||
|
||||
expect($result['headingIsH2'])->toBeTrue();
|
||||
expect($result['headingBeforeTable'])->toBeTrue();
|
||||
expect($result['noCard'])->toBeTrue();
|
||||
expect($result['headingIsCardTitle'])->toBeTrue();
|
||||
expect($result['listInSameCard'])->toBeTrue();
|
||||
expect($result['headingBeforeList'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('the admin settings page has six headed sections and no card', function () {
|
||||
test('the admin settings page has six sections, each a card headed by an h2', function () {
|
||||
$this->actingAs(User::factory()->admin()->create());
|
||||
|
||||
$page = ready(visit('/admin/settings'));
|
||||
|
||||
$result = $page->script("(() => {
|
||||
// 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.
|
||||
const headings = [...document.querySelectorAll('h2:not([data-md-modal-title])')];
|
||||
const cards = [...document.querySelectorAll('[data-md-card]')];
|
||||
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,
|
||||
headings: headings.map((h) => h.textContent.trim()),
|
||||
cardCount: cards.length,
|
||||
everyCardHeaded: cards.every((card) => card.querySelector('h2[data-md-card-title]') !== null),
|
||||
};
|
||||
})()");
|
||||
|
||||
expect($result['h2Count'])->toBe(6);
|
||||
expect($result['noCard'])->toBeTrue();
|
||||
expect($result['headings'])->toBe(['Colour profile', 'Branding', 'Upload Protection', 'Share Passwords', 'Upload Limits', 'Storage']);
|
||||
expect($result['cardCount'])->toBe(6);
|
||||
expect($result['everyCardHeaded'])->toBeTrue();
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ test('admin can delete share', function () {
|
||||
expect(Share::query()->find($shareId))->toBeNull();
|
||||
});
|
||||
|
||||
test('admin dashboard shows shares table', function () {
|
||||
test('admin dashboard lists the shares', function () {
|
||||
$admin = User::query()->where('is_admin', true)->first();
|
||||
|
||||
$share = Share::factory()->create(['token' => 'testtoken12345678']);
|
||||
@@ -70,31 +70,47 @@ test('admin dashboard shows shares table', function () {
|
||||
|
||||
$response->assertOk();
|
||||
$response->assertSee('testtoken12345678');
|
||||
$response->assertSee('href="'.route('share.download', $share).'"', false);
|
||||
});
|
||||
|
||||
test('the shares table sorts only by its own columns', function () {
|
||||
test('the shares list sorts only by its own orders', function () {
|
||||
$admin = User::query()->where('is_admin', true)->first();
|
||||
|
||||
Share::factory()->create(['token' => 'aaaaaaaaaaaaaaaa', 'download_count' => 9]);
|
||||
Share::factory()->create(['token' => 'zzzzzzzzzzzzzzzz', 'download_count' => 1]);
|
||||
Share::factory()->create(['token' => 'aaaaaaaaaaaaaaaa', 'download_count' => 1, 'created_at' => now()->subDay()]);
|
||||
Share::factory()->create(['token' => 'zzzzzzzzzzzzzzzz', 'download_count' => 9, 'created_at' => now()->subDays(2)]);
|
||||
|
||||
Livewire::actingAs($admin)
|
||||
->test(AdminDashboard::class)
|
||||
->set('sortBy', ['column' => 'download_count', 'direction' => 'asc'])
|
||||
->assertSeeInOrder(['aaaaaaaaaaaaaaaa', 'zzzzzzzzzzzzzzzz'])
|
||||
->set('sort', 'most-downloaded')
|
||||
->assertSeeInOrder(['zzzzzzzzzzzzzzzz', 'aaaaaaaaaaaaaaaa'])
|
||||
->set('sortBy', ['column' => 'token; drop table shares', 'direction' => 'sideways'])
|
||||
->assertOk();
|
||||
->set('sort', 'token; drop table shares')
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['aaaaaaaaaaaaaaaa', 'zzzzzzzzzzzzzzzz']);
|
||||
|
||||
expect(Share::query()->count())->toBe(2);
|
||||
});
|
||||
|
||||
test('without shares the dashboard shows an empty state instead of the table', function () {
|
||||
test('sorting by expiry puts shares that never expire last', function () {
|
||||
$admin = User::query()->where('is_admin', true)->first();
|
||||
|
||||
Share::factory()->create(['token' => 'neverexpires0000', 'expires_at' => null]);
|
||||
Share::factory()->create(['token' => 'expireslater0000', 'expires_at' => now()->addWeek()]);
|
||||
Share::factory()->create(['token' => 'expiressoon00000', 'expires_at' => now()->addHour()]);
|
||||
|
||||
Livewire::actingAs($admin)
|
||||
->test(AdminDashboard::class)
|
||||
->set('sort', 'expiring')
|
||||
->assertSeeInOrder(['expiressoon00000', 'expireslater0000', 'neverexpires0000']);
|
||||
});
|
||||
|
||||
test('without shares the dashboard shows an empty state instead of the list', function () {
|
||||
$admin = User::query()->where('is_admin', true)->first();
|
||||
|
||||
$this->actingAs($admin)->get(route('admin.dashboard'))
|
||||
->assertOk()
|
||||
->assertSee('No shares yet')
|
||||
->assertDontSee('<table', false);
|
||||
->assertDontSee('data-test="share-row"', false);
|
||||
});
|
||||
|
||||
test('shares whose files are still uploading are neither listed nor counted, but their bytes count as used space', function () {
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Setting;
|
||||
use App\Models\Share;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* The opening tag of the page template's pane (resources/views/components/page.blade.php).
|
||||
*/
|
||||
function pageTag(string $html): string
|
||||
{
|
||||
preg_match('/<div\s[^>]*data-test="page"[^>]*>/', $html, $matches);
|
||||
|
||||
return $matches[0] ?? '';
|
||||
}
|
||||
|
||||
test('every page is one page template, one h1 and the same width', function (Closure $visit) {
|
||||
$html = $visit($this)->assertOk()->getContent();
|
||||
|
||||
expect(substr_count($html, 'data-test="page"'))->toBe(1)
|
||||
->and(preg_match_all('/<h1[\s>]/', $html))->toBe(1)
|
||||
->and(pageTag($html))->toContain('data-md-width="narrow"');
|
||||
})->with([
|
||||
'upload' => [fn (TestCase $test): TestResponse => $test->get(route('upload'))],
|
||||
'download' => [fn (TestCase $test): TestResponse => $test->get(route('share.download', Share::factory()->withPassword()->create()))],
|
||||
'share created' => [fn (TestCase $test): TestResponse => $test->get(route('share.created', Share::factory()->create()))],
|
||||
'login' => [fn (TestCase $test): TestResponse => $test->get(route('login'))],
|
||||
'forgot password' => [fn (TestCase $test): TestResponse => $test->get(route('password.request'))],
|
||||
'reset password' => [fn (TestCase $test): TestResponse => $test->get(route('password.reset', 'token'))],
|
||||
'two-factor challenge' => [fn (TestCase $test): TestResponse => $test->withSession(['login.id' => User::factory()->withTwoFactor()->create()->id])->get(route('two-factor.login'))],
|
||||
'setup' => [function (TestCase $test): TestResponse {
|
||||
User::query()->where('is_admin', true)->delete();
|
||||
|
||||
return $test->get(route('setup'));
|
||||
}],
|
||||
'system password' => [function (TestCase $test): TestResponse {
|
||||
Setting::set('system_password', bcrypt('system-secret'));
|
||||
|
||||
return $test->get(route('system-password'));
|
||||
}],
|
||||
'verify email' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->unverified()->create())->get(route('verification.notice'))],
|
||||
'confirm password' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->create())->get(route('password.confirm'))],
|
||||
'profile' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->create())->get(route('profile.edit'))],
|
||||
'password' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->create())->get(route('user-password.edit'))],
|
||||
'two-factor settings' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->create())->withSession(['auth.password_confirmed_at' => time()])->get(route('two-factor.show'))],
|
||||
'appearance' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->create())->get(route('appearance.edit'))],
|
||||
'admin settings' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->admin()->create())->get(route('admin.settings'))],
|
||||
'admin dashboard' => [fn (TestCase $test): TestResponse => $test->actingAs(User::factory()->admin()->create())->get(route('admin.dashboard'))],
|
||||
]);
|
||||
|
||||
test("a brand page is headed by the site's own logo, title and description", function () {
|
||||
Setting::set('site_title', 'Acme Files');
|
||||
Setting::set('site_description', 'Send files to Acme Engineering.');
|
||||
Setting::set('site_logo', 'branding/acme.png');
|
||||
|
||||
$html = $this->get(route('login'))
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['data-test="page-logo"', '<h1', 'Send files to Acme Engineering.'], false)
|
||||
->assertSee(Storage::disk('public')->url('branding/acme.png'), false)
|
||||
->getContent();
|
||||
|
||||
expect($html)->toMatch('/<h1[^>]*>\s*Acme Files\s*<\/h1>/');
|
||||
});
|
||||
|
||||
test('a brand page falls back to the app name and SealShare\'s own line without branding', function () {
|
||||
$html = $this->get(route('upload'))
|
||||
->assertOk()
|
||||
->assertSeeInOrder(['<h1', 'Share your files safely and securely'], false)
|
||||
->assertDontSee('data-test="page-logo"', false)
|
||||
->getContent();
|
||||
|
||||
expect($html)->toMatch('/<h1[^>]*>\s*'.preg_quote(config('app.name'), '/').'\s*<\/h1>/');
|
||||
});
|
||||
Reference in New Issue
Block a user