diff --git a/app/Devnull/Stash.php b/app/Devnull/Stash.php index 899db42..cd2ed6d 100644 --- a/app/Devnull/Stash.php +++ b/app/Devnull/Stash.php @@ -7,8 +7,8 @@ /** * What you wrote and didn't send, per user, as the web's useStash keeps it. * - * Drafts go by the web's names: 'commit' for the Log's composer, which its - * sheet shares, 'quote-{id}' for a quote of a commit and 'reply-{id}' for a + * Drafts go by the web's names: 'commit' for a new commit, from the compose + * sheet, 'quote-{id}' for a quote of a commit and 'reply-{id}' for a * reply to one. A screen keeps a draft as its field changes, and forgets it * once it's sent or the field is emptied. Signing out forgets them all (see * Account::forget). diff --git a/app/NativeComponents/Concerns/OpensCommitSheet.php b/app/NativeComponents/Concerns/OpensCommitSheet.php index b6db9f0..d919c90 100644 --- a/app/NativeComponents/Concerns/OpensCommitSheet.php +++ b/app/NativeComponents/Concerns/OpensCommitSheet.php @@ -20,9 +20,9 @@ * your new commits puts it in place (see showCommitted()). * * What you write in it is stashed as you type, as on the web (see Stash): a - * quote per commit quoted, and a new commit as the Log's composer's draft, - * which the sheet shares, as the web's dialog does. Opened again, it starts - * from the draft, marked as restored. It's forgotten once it's sent. + * quote per commit quoted, and a new commit under the web's composer draft. + * Opened again, it starts from the draft, marked as restored. It's forgotten + * once it's sent. * * The sheet keeps its commit while it slides away, so it doesn't empty on the * way down, and lets go of it once iOS says it's gone, however it was closed. @@ -202,22 +202,7 @@ public function hideCommitSheet(): void */ public function commitSheetClosed(): void { - $composed = $this->sheetView === 'compose'; - $this->forgetCommitSheet(); - - if ($composed) { - $this->composeSheetClosed(); - } - } - - /** - * The sheet's new commit has gone, sent or stashed. The Log's composer shares its draft, so it takes it as the - * sheet left it. - */ - protected function composeSheetClosed(): void - { - // } /** diff --git a/app/NativeComponents/Log.php b/app/NativeComponents/Log.php index 294fe7f..43ed179 100644 --- a/app/NativeComponents/Log.php +++ b/app/NativeComponents/Log.php @@ -28,25 +28,19 @@ /** * The log: the web's three of them, Following (you and the people you follow), - * Latest (everyone, newest first) and Trending (ranked by stars and replies), - * under a composer you type into where it is, as on the web. + * Latest (everyone, newest first) and Trending (ranked by stars and replies). * - * EDGE tells PHP nothing about focus, and gives PHP no way to focus a field - * or scroll a list. So a touch on the field is taken as being in it, and a new - * key on the list is how it gets back to the top. + * EDGE gives PHP no way to scroll a list, so a new key on the list is how it + * gets back to the top. * - * Nor does it say where a list is scrolled, so the nav bar's compose button - * can't tell whether the composer is on screen. It always opens the composer - * in a sheet (see OpensCommitSheet), which leaves the log where you were in - * it, and the commit goes on top as one from the composer does. - * - * The composer's draft is stashed as you type, as on the web (see Stash), so - * it's there again when the Log opens, marked as restored. The sheet shares - * it, as the web's dialog does, and the composer takes what the sheet left. + * The Log has no composer of its own. The nav bar's compose button opens one + * in a sheet (see OpensCommitSheet), which leaves the log where you were in it, + * and the commit goes on top. A field in the list was redrawn with the whole + * log every time it synced, and fast typing lost and reordered letters. * * It opens on the log as it was last time, then asks devnull again in the * background, and every 30 seconds while it's showing, as the web polls. - * Commits newer than anything showing wait under the composer in the new + * Commits newer than anything showing wait at the top of the list in the new * commits pill (see HoldsNewCommits), which says how many devnull counted * since the newest one showing (?since=). Scrolled down, it pins under the tabs. * @@ -144,31 +138,8 @@ class Log extends NativeComponent */ public ?string $lastVisitAt = null; - /** - * The commit you're writing. It stays when the keyboard goes, until it's committed. - */ - public string $body = ''; - - /** - * Whether the composer shows its budget and Commit button: from the moment you're - * in it until you commit, or leave it empty and the log reloads. - */ - public bool $composing = false; - - /** - * Mounts a fresh field when it changes, so a committed field starts clean. - */ - public int $composerKey = 0; - - /** - * Whether the draft in the composer was stashed when it opened, which it marks until you empty it or drop it. - */ - public bool $draftRestored = false; - public function mount(): void { - $this->body = app(Stash::class)->get($this->me()?->id, 'commit'); - $this->draftRestored = $this->body !== ''; $this->log = app(ShownTabs::class)->get($this->me()?->id, 'log', self::Logs, 'following'); $this->lastVisitAt = $this->lastVisitSeenAt(); $this->showKept(); @@ -181,7 +152,6 @@ public function mount(): void public function onResume(): void { $this->forgetRefreshes(); - $this->closeEmptyComposer(); $this->forgetDeleted(); $this->forgetHidden(); $this->showKept(); @@ -323,8 +293,6 @@ public function isRanked(): bool */ public function load(): void { - $this->closeEmptyComposer(); - if ($this->isRanked()) { $this->rank(); @@ -432,88 +400,7 @@ public function followSuggested(string $username): void } /** - * A touch on the composer's field puts you in it. - */ - public function composerFocused(): void - { - $this->composing = true; - } - - /** - * Stashes the draft as it changes. An empty field forgets it, and isn't restored any more. - */ - public function updatedBody(): void - { - app(Stash::class)->put($this->me()?->id, 'commit', $this->body); - - if ($this->body === '') { - $this->draftRestored = false; - } - } - - /** - * The web's drop, beside a restored draft: it's thrown away, and a fresh field takes its place. - */ - public function dropStash(): void - { - $this->body = ''; - $this->updatedBody(); - $this->composerKey++; - } - - public function remaining(): int - { - return Commit::MaxLength - mb_strlen($this->body); - } - - public function canCommit(): bool - { - $length = mb_strlen(trim($this->body)); - - return $length >= Commit::MinLength && $length <= Commit::MaxLength; - } - - /** - * Commits the draft and puts it at the top of the log. Pressing Commit has - * already put the keyboard away. The draft stays if devnull says no. - */ - public function commit(): void - { - if (! $this->canCommit()) { - return; - } - - $commit = $this->attempt(fn ($devnull) => $devnull->createCommit(trim($this->body))); - - if ($commit === null) { - return; - } - - $this->body = ''; - $this->updatedBody(); - $this->composing = false; - $this->composerKey++; - $this->showCommitted($commit); - } - - /** - * The sheet wrote to the composer's stash, so the composer takes the draft as the sheet left it, in a fresh field. - */ - protected function composeSheetClosed(): void - { - $draft = app(Stash::class)->get($this->me()?->id, 'commit'); - - if ($draft === $this->body) { - return; - } - - $this->body = $draft; - $this->draftRestored = $draft !== ''; - $this->composerKey++; - } - - /** - * Puts a commit you just made at the top of the log, from the composer or the sheet. + * Puts a commit you just made in the compose sheet at the top of the log. */ protected function showCommitted(Commit $commit): void { @@ -546,20 +433,6 @@ protected function updateShownCommits(Closure $update): void $this->threads = array_map(fn (array $replies): array => array_map($update, $replies), $this->threads); } - /** - * An empty composer you've left closes again. Nothing says when you leave it, - * so this waits for the log to reload: on returning to it, or pulling to refresh. - */ - private function closeEmptyComposer(): void - { - if (! $this->composing || $this->body !== '') { - return; - } - - $this->composing = false; - $this->composerKey++; - } - /** * The log as it was kept last time, if there is one. Coming back to it, it's * laid over what's showing like any fresh page, so a change made elsewhere @@ -772,9 +645,6 @@ public function render(): View 'myColour' => $this->myColour(), 'viewer' => $viewer, 'pasteis' => app(Account::class)->pasteis(), - 'composerOpen' => $this->composing || $this->body !== '', - 'remaining' => $this->remaining(), - 'canCommit' => $this->canCommit(), 'stashed' => app(Stash::class)->names($viewer?->id), 'newCommitsLabel' => $this->newCommitsLabel(), 'pages' => $pages, diff --git a/resources/views/native/partials/composer.blade.php b/resources/views/native/partials/composer.blade.php deleted file mode 100644 index f9f60ae..0000000 --- a/resources/views/native/partials/composer.blade.php +++ /dev/null @@ -1,48 +0,0 @@ -{{-- - The top of the log, like the web's composer: your avatar staged on the rail beside "What did you ship?", which you - type into where it is. Once you're in it, or it holds a draft, Commit sits under it, with the web's counter once - few of the 4,000 characters are left. The ring is dashed until the commit is long enough to make, then solid. - - The field is keyed, so a new $composerKey (or a new list key) mounts a fresh one, clean after a commit. A touch on - it is how the screen learns you're in it, since a field reports neither focus nor, on iOS, a caret in an empty - field. The nav bar's compose button opens a composer in the sheet instead (see OpensCommitSheet). - - A draft stashed last time comes back in it, with the web's stash@{0} and drop on the left of Commit's row. ---}} - - - - - - - - - - @if ($composerOpen) - - @if ($draftRestored) - @include('native.partials.stash-label', ['ref' => 'stash', 'drop' => 'dropStash']) - - @endif - @include('native.partials.body-counter', ['body' => $body, 'ref' => 'budget']) - Commit - - @endif - - - - - - @include('native.partials.rail', ['lane' => $me->lane(), 'user' => $me, 'node' => $canCommit ? 'avatar' : 'staged', 'tail' => 'staged', 'tailDashes' => $composerOpen ? 6 : 2]) - diff --git a/resources/views/native/partials/log-page.blade.php b/resources/views/native/partials/log-page.blade.php index 92092df..9dcfa7c 100644 --- a/resources/views/native/partials/log-page.blade.php +++ b/resources/views/native/partials/log-page.blade.php @@ -19,10 +19,6 @@ class="w-full flex-1 bg-theme-surface" :on-end-reached="$page['showing'] ? 'loadMore' : null" :end-reached-buffer="5" > - @if ($viewer) - @include('native.partials.composer', ['me' => $viewer]) - @endif - @include('native.partials.new-commits-pill', ['label' => $newCommitsLabel, 'press' => 'showNewCommits', 'shown' => $page['showing'] && $newCommits !== []]) @if (! $page['loaded'] && $page['error'] === null) diff --git a/tests/Feature/Screens/ComposeSheetTest.php b/tests/Feature/Screens/ComposeSheetTest.php index 22547ad..f3f2aae 100644 --- a/tests/Feature/Screens/ComposeSheetTest.php +++ b/tests/Feature/Screens/ComposeSheetTest.php @@ -50,9 +50,8 @@ function sheetCommitDisabled(TestableComponent $screen): bool 'autofocus' => true, 'value' => '', ]) - // The same list, so it stays scrolled where it was, and the composer at its top stays as it was. - ->and(nodeByRef($screen, 'log')['id'])->toBe($list) - ->and($screen->instance()->composing)->toBeFalse(); + // The same list, so it stays scrolled where it was. + ->and(nodeByRef($screen, 'log')['id'])->toBe($list); }); it('keeps Commit disabled until the commit is long enough, and short enough', function () { @@ -139,39 +138,6 @@ function sheetCommitDisabled(TestableComponent $screen): bool Http::assertNotSent(fn (Request $request): bool => $request->method() === 'POST'); }); -it('shares its draft with the composer at the top of the log, as the web dialog does', function () { - fakeApi([ - 'GET /api/v1/log/following' => [pageJson([])], - 'POST /api/v1/commits' => [['data' => commitJson(['id' => 'mine', 'body' => 'Half a thought, shipped.'])], 201], - ]); - - $screen = Native::visit('/log', platform: 'ios') - ->input('body', 'Half a thought') - ->press('composeInSheet'); - - expect(nodeByRef($screen, 'sheet-body')['props']['value'])->toBe('Half a thought'); - - // Closed, the composer takes the draft as the sheet left it. - $screen->input('sheet-body', 'Half a thought, and more') - ->dismissSheet('sheet'); - - expect(nodeByRef($screen, 'body')['props']['value'])->toBe('Half a thought, and more') - ->and(nodeByRef($screen, 'stash'))->not->toBeEmpty(); - - // Sent from the sheet, it's gone from both. - $screen->press('composeInSheet') - ->input('sheet-body', 'Half a thought, shipped.') - ->tap('sheet-commit') - ->dismissSheet('sheet') - ->assertSet('body', ''); - - expect(nodeByRef($screen, 'body')['props']['value'])->toBe(''); - - $screen->press('composeInSheet'); - - expect(nodeByRef($screen, 'sheet-body')['props']['value'])->toBe(''); -}); - it('only commits from a sheet that has a composer in it', function () { fakeApi(['GET /api/v1/log/following' => [pageJson([commitJson(['id' => 'c1', 'author' => userJson(['id' => 'u2', 'username' => 'ada'])])])]]); diff --git a/tests/Feature/Screens/ComposerTest.php b/tests/Feature/Screens/ComposerTest.php deleted file mode 100644 index c99125e..0000000 --- a/tests/Feature/Screens/ComposerTest.php +++ /dev/null @@ -1,224 +0,0 @@ - 'http://devnull.test']); - signIn(); -}); - -function composerBudget(): Closure -{ - return fn (string $text, string $color): Closure => fn (array $node): bool => ($node['ref'] ?? null) === 'budget' - && ($node['text'] ?? $node['props']['text'] ?? null) === $text - && ($node['props']['color'] ?? null) === config("native-ui.theme.light.{$color}"); -} - -function commitButton(bool $disabled): Closure -{ - return fn (array $node): bool => ($node['ref'] ?? null) === 'commit' && ($node['props']['disabled'] ?? false) === $disabled; -} - -/** - * The composer's field, as the native side would mount it. - * - * @return array - */ -function composerField(TestableComponent $screen): array -{ - return nodeByRef($screen, 'body'); -} - -it('is a field at the top of the log, with no budget or Commit until you are in it', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson()]))]); - - Native::visit('/log') - ->assertElement('bare_text_input', fn (array $node): bool => ($node['ref'] ?? null) === 'body' - && ($node['props']['placeholder'] ?? null) === 'What did you ship?' - && ($node['props']['max_length'] ?? null) === 4000 - && ($node['props']['multiline'] ?? false) - && ! isset($node['props']['autofocus'])) - ->assertMissingElement('text', fn (array $node): bool => ($node['ref'] ?? null) === 'budget') - ->assertMissingElement('button', fn (array $node): bool => ($node['ref'] ?? null) === 'commit'); -}); - -it('opens where it is when you tap into it, with a disabled Commit, no countdown yet, and no navigation', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson()]))]); - - Native::visit('/log') - ->pressUp('composer-field') - ->assertNoNavigation() - ->assertSet('composing', true) - ->assertMissingElement('text', fn (array $node): bool => ($node['ref'] ?? null) === 'budget') - ->assertElement('button', commitButton(disabled: true)); -}); - -it('stays open once it has a draft', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson()]))]); - - Native::visit('/log') - ->input('body', 'Shipped it') - ->assertElement('button', commitButton(disabled: false)); -}); - -it('counts down like the web only once 200 characters are left, amber from 20 and red at none', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson()]))]); - - Native::visit('/log') - ->pressUp('composer-field') - ->input('body', str_repeat('x', 3799)) - ->assertMissingElement('text', fn (array $node): bool => ($node['ref'] ?? null) === 'budget') - ->input('body', str_repeat('x', 3800)) - ->assertElement('text', composerBudget()('200', 'ink-muted')) - ->assertElement('text', fn (array $node): bool => ($node['ref'] ?? null) === 'budget' && ($node['props']['font_name'] ?? $node['props']['font'] ?? null) !== null) - ->input('body', str_repeat('x', 3980)) - ->assertElement('text', composerBudget()('20', 'pending')) - ->input('body', str_repeat('x', 4000)) - ->assertElement('text', composerBudget()('0', 'destructive')) - ->assertElement('button', commitButton(disabled: false)); -}); - -it('shows the fold icon once a commit is long enough for lists to show only its start', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson()]))]); - - $fold = fn (array $node): bool => ($node['ref'] ?? null) === 'budget-fold' && ($node['props']['name'] ?? null) === 'custom:fold-vertical'; - - Native::visit('/log', platform: 'ios') - ->pressUp('composer-field') - ->input('body', str_repeat('x', 280)) - ->assertMissingElement('icon', $fold) - ->input('body', str_repeat('x', 281)) - ->assertElement('icon', $fold) - ->input('body', implode("\n", range(1, 9))) - ->assertElement('icon', $fold); -}); - -it('enables Commit once the commit is long enough', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson()]))]); - - Native::visit('/log') - ->pressUp('composer-field') - ->input('body', 'hi') - ->assertElement('button', commitButton(disabled: true)) - ->input('body', ' hi ') - ->assertElement('button', commitButton(disabled: true)) - ->input('body', 'hi!') - ->assertElement('button', commitButton(disabled: false)); -}); - -it('stages your avatar on the rail in a dashed ring, solid once the commit is long enough', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson(['author' => userJson(['id' => 'u2', 'username' => 'ada', 'lane' => 1])])]))]); - - $lane = config('native-ui.theme.light.lane-3'); - $solidRing = fn (array $node): bool => ($node['type'] ?? null) === 'stack' && ($node['layout']['width'] ?? null) === 48.0 && ($node['style']['bg_color'] ?? null) === $lane; - $stagedRing = fn (array $node): bool => ($node['ref'] ?? null) === 'staged-avatar'; - - Native::visit('/log') - ->assertElement('column', $stagedRing) - ->assertMissingElement('stack', $solidRing) - ->input('body', 'hi') - ->assertElement('column', $stagedRing) - ->input('body', 'hi!') - ->assertMissingElement('column', $stagedRing) - ->assertElement('stack', $solidRing); -}); - -it('commits without leaving the log, then clears the composer and puts the commit at the top', function () { - fakeApi([ - 'GET /api/v1/log/following' => [pageJson([commitJson(['id' => 'c1', 'body' => 'An older commit', 'author' => userJson(['id' => 'u2', 'username' => 'ada'])])])], - 'POST /api/v1/commits' => [['data' => commitJson(['id' => '0199e5c4-1111-7000-8000-00000000beef', 'body' => 'Shipped the mobile app.'])], 201], - ]); - - $screen = Native::visit('/log')->pressUp('composer-field')->input('body', ' Shipped the mobile app. '); - $draftField = composerField($screen)['id']; - - $screen->tap('commit') - ->assertNoNavigation() - ->assertNativeNotCalled('Dialog.Toast') - ->assertSet('sheetOpen', false) - ->assertSet('body', '') - ->assertSet('composing', false) - ->assertSee('Shipped the mobile app.') - ->assertMissingElement('text', fn (array $node): bool => ($node['ref'] ?? null) === 'budget') - ->assertMissingElement('button', fn (array $node): bool => ($node['ref'] ?? null) === 'commit'); - - // A fresh, empty field: the committed one is gone, so the next tap into it opens it again. - expect(composerField($screen)['id'])->not->toBe($draftField) - ->and(composerField($screen)['props']['value'] ?? null)->toBe('') - ->and(array_map(fn ($commit): string => $commit->id, $screen->instance()->commits))->toBe(['0199e5c4-1111-7000-8000-00000000beef', 'c1']); - - $json = json_encode($screen->tree()); - expect(strpos($json, 'commit-0199e5c4-1111-7000-8000-00000000beef'))->toBeLessThan(strpos($json, 'commit-c1')); - - Http::assertSent(fn (Request $request): bool => $request->method() === 'POST' && $request->body() === '{"body":"Shipped the mobile app."}'); -}); - -it('marks the new commit as pending on its row', function () { - fakeApi([ - 'GET /api/v1/log/following' => [pageJson([])], - 'POST /api/v1/commits' => [['data' => commitJson(['id' => 'abc1234', 'body' => 'Needs a look first', 'status' => 'pending'])], 201], - ]); - - Native::visit('/log') - ->input('body', 'Needs a look first') - ->tap('commit') - ->assertNativeNotCalled('Dialog.Toast') - ->assertNoNavigation() - ->assertSee('Needs a look first') - ->assertSee('pending'); -}); - -it('keeps the draft and shows the reason when the server says no', function () { - fakeApi([ - 'GET /api/v1/log/following' => [pageJson([])], - 'POST /api/v1/commits' => [['message' => 'Too Many Attempts.'], 429], - ]); - - Native::visit('/log') - ->input('body', 'One more') - ->tap('commit') - ->assertSee('Too Many Attempts.') - ->assertSet('body', 'One more') - ->assertElement('button', commitButton(disabled: false)) - ->assertNoNavigation(); -}); - -it('sends nothing shorter than three characters', function () { - fakeApi(['GET /api/v1/log/following' => [pageJson([])]]); - - Native::visit('/log')->input('body', 'ok')->call('commit')->assertSet('body', 'ok'); - - Http::assertNotSent(fn (Request $request): bool => $request->method() === 'POST'); -}); - -it('syncs the draft on a short debounce so a burst of typing arrives whole', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([]))]); - - // In live mode a burst of keystrokes can overflow the native event queue, - // and the input never resends the dropped tail. Debounce sends one trailing value. - Native::visit('/log')->assertElement('bare_text_input', fn (array $node): bool => ($node['ref'] ?? null) === 'body' - && ($node['props']['sync_mode'] ?? null) === 'debounce' - && ($node['props']['debounce_ms'] ?? null) === 150); -}); - -it('closes an empty composer when the log reloads, and keeps one with a draft open', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([]))]); - - Native::visit('/log') - ->pressUp('composer-field') - ->assertSet('composing', true) - ->call('load') - ->assertSet('composing', false) - ->assertMissingElement('button', fn (array $node): bool => ($node['ref'] ?? null) === 'commit'); - - Native::visit('/log') - ->pressUp('composer-field') - ->input('body', 'Still thinking') - ->call('load') - ->assertSet('composing', true) - ->assertSet('body', 'Still thinking') - ->assertElement('button', commitButton(disabled: false)); -}); diff --git a/tests/Feature/Screens/DraftStashTest.php b/tests/Feature/Screens/DraftStashTest.php index fc50ea6..53941ce 100644 --- a/tests/Feature/Screens/DraftStashTest.php +++ b/tests/Feature/Screens/DraftStashTest.php @@ -72,79 +72,19 @@ function stashLabel(TestableComponent $screen, string $ref): array // ── The Log's composer ───────────────────────── -it("stashes the Log's composer as you type, and brings it back marked as restored when the Log opens again", function () { - serveStashLog(); - - $screen = Native::visit('/log', platform: 'ios')->input('body', 'Half a thought'); - - expect(stashed('commit'))->toBe('Half a thought') - ->and(showsRef($screen, 'stash'))->toBeFalse(); - - $screen = Native::visit('/log', platform: 'ios'); - - expect(nodeByRef($screen, 'body')['props']['value'])->toBe('Half a thought') - ->and(stashLabel($screen, 'stash'))->toBe(['stash@{0}', 'Discard the restored draft']) - ->and(nodeByRef($screen, 'commit')['props']['disabled'] ?? false)->toBeFalse(); -}); - -it("forgets the composer's stash once the commit is made", function () { - serveStashLog(); - stash('commit', 'Sent.'); - - $screen = Native::visit('/log', platform: 'ios') - ->tap('commit') - ->assertSet('body', ''); - - expect(stashed('commit'))->toBe('') - ->and(showsRef($screen, 'stash'))->toBeFalse(); - - expect(nodeByRef(Native::visit('/log', platform: 'ios'), 'body')['props']['value'])->toBe(''); -}); - -it('forgets the stash when the field is emptied, and stops marking the draft as restored', function () { - serveStashLog(); - stash('commit', 'Half a thought'); - - $screen = Native::visit('/log', platform: 'ios')->input('body', ''); - - expect(stashed('commit'))->toBe('') - ->and(showsRef($screen, 'stash'))->toBeFalse(); - - $screen->input('body', 'A new thought'); - - expect(stashed('commit'))->toBe('A new thought') - ->and(showsRef($screen, 'stash'))->toBeFalse(); -}); - -it('drops a restored draft, leaving a fresh empty field', function () { - serveStashLog(); - stash('commit', 'Half a thought'); - - $screen = Native::visit('/log', platform: 'ios') - ->tap('stash-drop') - ->assertSet('body', '') - ->assertSet('composerKey', 1); - - expect(stashed('commit'))->toBe('') - ->and(nodeByRef($screen, 'body')['props']['value'])->toBe('') - ->and(showsRef($screen, 'stash'))->toBeFalse(); - - Http::assertNotSent(fn (Request $request): bool => $request->method() === 'POST'); -}); - it('keeps your drafts to you', function () { serveStashLog(); app(Stash::class)->put('u2', 'commit', "Ada's draft"); - $screen = Native::visit('/log', platform: 'ios'); + $screen = Native::visit('/log', platform: 'ios')->press('composeInSheet'); - expect(nodeByRef($screen, 'body')['props']['value'])->toBe('') - ->and(showsRef($screen, 'stash'))->toBeFalse(); + expect(nodeByRef($screen, 'sheet-body')['props']['value'])->toBe('') + ->and(showsRef($screen, 'sheet-stash'))->toBeFalse(); }); // ── The compose sheet ────────────────────────── -it("stashes the compose sheet as the composer's draft, and forgets it once it's committed", function () { +it("stashes the compose sheet's draft, and forgets it once it's committed", function () { serveStashLog(); $screen = Native::visit('/log', platform: 'ios') @@ -153,21 +93,19 @@ function stashLabel(TestableComponent $screen, string $ref): array expect(stashed('commit'))->toBe('Written in the sheet'); - $screen->dismissSheet('sheet'); - - expect(nodeByRef($screen, 'body')['props']['value'])->toBe('Written in the sheet') - ->and(nodeByRef($screen, 'stash'))->not->toBeEmpty(); - - $screen->press('composeInSheet'); + $screen->dismissSheet('sheet')->press('composeInSheet'); expect(nodeByRef($screen, 'sheet-body')['props']['value'])->toBe('Written in the sheet') ->and(stashLabel($screen, 'sheet-stash'))->toBe(['stash@{0}', 'Discard the restored draft']); $screen->tap('sheet-commit')->dismissSheet('sheet'); - expect(stashed('commit'))->toBe('') - ->and(nodeByRef($screen, 'body')['props']['value'])->toBe('') - ->and(showsRef($screen, 'stash'))->toBeFalse(); + expect(stashed('commit'))->toBe(''); + + $screen->press('composeInSheet'); + + expect(nodeByRef($screen, 'sheet-body')['props']['value'])->toBe('') + ->and(showsRef($screen, 'sheet-stash'))->toBeFalse(); }); it("drops the sheet's restored draft", function () { @@ -181,10 +119,6 @@ function stashLabel(TestableComponent $screen, string $ref): array expect(stashed('commit'))->toBe('') ->and(showsRef($screen, 'sheet-stash'))->toBeFalse(); - - $screen->dismissSheet('sheet'); - - expect(nodeByRef($screen, 'body')['props']['value'])->toBe(''); }); // ── Quotes ───────────────────────────────────── diff --git a/tests/Feature/Screens/LaneButtonsTest.php b/tests/Feature/Screens/LaneButtonsTest.php index d3640f9..e01a4e0 100644 --- a/tests/Feature/Screens/LaneButtonsTest.php +++ b/tests/Feature/Screens/LaneButtonsTest.php @@ -53,7 +53,6 @@ function laneLog(): TestableComponent } dataset('main buttons', [ - 'Commit, in the composer' => [fn (): TestableComponent => laneLog()->pressUp('composer-field'), 'commit'], 'Commit, in the compose sheet' => [fn (): TestableComponent => laneLog()->press('composeInSheet'), 'sheet-commit'], 'Commit, quoting a commit' => [fn (): TestableComponent => laneLog()->tap('quote-c1'), 'sheet-commit'], 'Send report' => [fn (): TestableComponent => laneLog()->tap('report-c1'), 'send-report'], @@ -180,8 +179,7 @@ function laneLog(): TestableComponent expect(buttonProps($report, 'report-done')['size'] ?? null)->toBe('lg'); - expect(buttonProps(laneLog()->pressUp('composer-field'), 'commit'))->not->toHaveKey('size') - ->and(buttonProps(laneLog()->tap('quote-c1'), 'sheet-commit'))->not->toHaveKey('size'); + expect(buttonProps(laneLog()->tap('quote-c1'), 'sheet-commit'))->not->toHaveKey('size'); }); it('makes Unlink Apple ID large too', function () { diff --git a/tests/Feature/Screens/LogCacheTest.php b/tests/Feature/Screens/LogCacheTest.php index e1e7e13..641417b 100644 --- a/tests/Feature/Screens/LogCacheTest.php +++ b/tests/Feature/Screens/LogCacheTest.php @@ -284,7 +284,7 @@ function shownCommitIds(TestableComponent $screen): array 'POST /api/v1/commits' => [['data' => commitJson(['id' => 'c9', 'body' => 'Just shipped'])], 201], ]); - Native::visit('/log')->pressUp('composer-field')->input('body', 'Just shipped')->tap('commit'); + Native::visit('/log')->press('composeInSheet')->input('sheet-body', 'Just shipped')->tap('sheet-commit'); holdRefreshes(); @@ -322,7 +322,7 @@ function shownCommitIds(TestableComponent $screen): array $commit->goBack()->assertDontSee('Delete me')->assertSee('Keep me'); }); -it('puts the pill in the list between the composer and the commits, in a header that pins, there before it shows', function () { +it('puts the pill at the top of the list, above the commits, in a header that pins, there before it shows', function () { holdRefreshes(); keep('log/following', pageJson([commitJson(['id' => 'c1'])])); fakeFreshLog([commitJson(['id' => 'c2', 'body' => 'Newer']), commitJson(['id' => 'c1'])]); @@ -334,11 +334,11 @@ function shownCommitIds(TestableComponent $screen): array return array_map(fn (array $node): string => $node['ref'] ?? $node['type'], $parent['children']); }; - $inTheList = fn (): array => array_slice(array_values(array_filter(childRefs($screen, 'log'), fn (?string $ref): bool => $ref !== null && ! str_starts_with($ref, 'day-'))), 0, 3); + $inTheList = fn (): array => array_slice(array_values(array_filter(childRefs($screen, 'log'), fn (?string $ref): bool => $ref !== null && ! str_starts_with($ref, 'day-'))), 0, 2); // Hidden and empty until there are new commits, but already in its place, so showing it never changes the list's shape. expect($besideTheList())->toBe(['log']) - ->and($inTheList())->toBe(['composer', 'new-commits-slot', 'commit-c1']) + ->and($inTheList())->toBe(['new-commits-slot', 'commit-c1']) ->and(nodeByRef($screen, 'new-commits-slot')['type'])->toBe('sticky_header') ->and(nodeByRef($screen, 'new-commits-slot')['layout']['display'] ?? null)->toBe(1) ->and(nodeByRef($screen, 'new-commits-slot')['children'] ?? [])->toBe([]); @@ -346,7 +346,7 @@ function shownCommitIds(TestableComponent $screen): array finishRefreshes($screen)->assertSee('1 new commit'); expect($besideTheList())->toBe(['log']) - ->and($inTheList())->toBe(['composer', 'new-commits-slot', 'commit-c1']) + ->and($inTheList())->toBe(['new-commits-slot', 'commit-c1']) ->and(nodeByRef($screen, 'new-commits-slot')['layout']['display'] ?? null)->toBeNull() ->and(findNode(nodeByRef($screen, 'new-commits-slot'), fn (array $node): bool => ($node['ref'] ?? null) === 'new-commits'))->not->toBeNull(); }); diff --git a/tests/Feature/Screens/LogTest.php b/tests/Feature/Screens/LogTest.php index 2fba789..51e78c4 100644 --- a/tests/Feature/Screens/LogTest.php +++ b/tests/Feature/Screens/LogTest.php @@ -502,18 +502,6 @@ ->assertNoNavigation(); }); -it('starts the log with your avatar staged on the rail, like the web composer', function () { - Http::fake(['devnull.test/*' => Http::response(pageJson([commitJson()]))]); - - $screen = Native::visit('/log')->assertSee('What did you ship?'); - - // A dashed ring in your lane: sixteen short dashes, each turned to follow the circle. - $dashes = findNodes(nodeByRef($screen, 'staged-avatar'), fn (array $node): bool => isset($node['props']['rotate'])); - - expect($dashes)->toHaveCount(16) - ->and(array_unique(array_map(fn (array $dash): ?string => $dash['style']['bg_color'] ?? null, $dashes)))->toBe([config('native-ui.theme.light.lane-3')]); -}); - it('shows an author once per run, and starts a new run at each day', function () { $this->travelTo(now()->setDate(2026, 10, 15)->setTime(12, 0)); $ada = userJson(['id' => 'u2', 'username' => 'ada', 'name' => 'Ada', 'lane' => 1]); diff --git a/tests/Feature/Screens/LogsTest.php b/tests/Feature/Screens/LogsTest.php index 1932011..57f87a3 100644 --- a/tests/Feature/Screens/LogsTest.php +++ b/tests/Feature/Screens/LogsTest.php @@ -305,7 +305,7 @@ function ada(): array $screen = Native::visit('/log'); $row = nodeByRef($screen, 'commit-c1')['id']; - $screen->pressUp('composer-field')->input('body', 'Fresh from the composer')->tap('commit'); + $screen->press('composeInSheet')->input('sheet-body', 'Fresh from the composer')->tap('sheet-commit'); expect(shownLogIds($screen))->toBe(['mine', 'c1']) ->and(nodeByRef($screen, 'commit-c1')['id'])->toBe($row); @@ -527,9 +527,9 @@ function ada(): array ]); $screen = tapLogTab(Native::visit('/log'), 'trending') - ->pressUp('composer-field') - ->input('body', 'Fresh from the composer') - ->tap('commit'); + ->press('composeInSheet') + ->input('sheet-body', 'Fresh from the composer') + ->tap('sheet-commit'); expect(shownLogIds($screen))->toBe(['mine', 'a']) ->and(dayMarkers($screen))->toBe([Timeline::JustNow, Timeline::TopCommits])