Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions app/Devnull/IssueCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

/**
* The web's `/feature` and `/bug` commands, which turn a new commit's draft
* into a public issue on nunomaduro/dev-null.sh, opened by devnull's GitHub
* into a public issue on dev-null-sh/dev-null.sh, opened by devnull's GitHub
* App and crediting you. The wording, and when each is suggested, are the
* web's (resources/js/lib/issue-commands.ts).
*/
Expand All @@ -20,7 +20,7 @@ enum IssueCommand: string
/**
* Where the issue goes, as the web says it under the command.
*/
public const string Repository = 'nunomaduro/dev-null.sh';
public const string Repository = 'dev-null-sh/dev-null.sh';

/**
* Issues still need three literal characters and do not discount URLs.
Expand Down
42 changes: 42 additions & 0 deletions app/Devnull/IssueEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Devnull;

use Native\Mobile\Facades\Device;
use Native\Mobile\Facades\System;

/**
* What maintainers need to reproduce an issue opened from the app, as the
* web's collectEnvironment() sends its browser and screen: the app and its
* version, the system and its version, the language and the appearance.
* devnull puts them in a collapsed Environment table under the issue.
*
* It's kept coarse, since the issue is public, so never the phone's name. A
* detail the phone doesn't give is left out, and none is longer than devnull
* takes, so a long one can't stop the issue opening.
*/
final class IssueEnvironment
{
public const int MaxLength = 100;

/**
* @return array<string, string>
*/
public static function collect(): array
{
$info = json_decode((string) Device::getInfo(), true);
$info = is_array($info) ? $info : [];
$text = fn (string $key): string => is_string($info[$key] ?? null) ? $info[$key] : '';

$environment = [
'browser' => 'devnull app '.config('nativephp.version'),
'os' => $text('operatingSystem').' '.$text('osVersion'),
'language' => $text('language'),
'color_scheme' => System::appearance(),
];

$environment = array_map(fn (string $value): string => mb_substr(trim($value), 0, self::MaxLength), $environment);

return array_filter($environment, fn (string $value): bool => $value !== '');
}
}
21 changes: 12 additions & 9 deletions app/NativeComponents/Concerns/DraftsIssues.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use App\Devnull\DraftImages;
use App\Devnull\Exceptions\ApiException;
use App\Devnull\IssueCommand;
use App\Devnull\IssueEnvironment;
use App\Devnull\SiteLink;
use App\Devnull\Stash;
use App\Devnull\Toasts;
Expand Down Expand Up @@ -37,14 +38,16 @@
* it, which offers the command with the draft kept. Its X stops the hints
* on this phone, as the web's stops them in that browser.
*
* Opening the issue waits on GitHub, so it's sent in the background while the
* button spins (see WritesInBackground). Once it's open the draft goes, with
* any photos it had, and the sheet says "Opened issue #123." with View issue,
* as the web's toast does, or a toast does, with View issue as its button, if
* the sheet was swiped away by then (see SyncsOutbox::toastActionPressed()).
* When it isn't, the draft stays an issue, with why under it: devnull's own
* words when it sends them, else the web's, which say to check upstream when
* the issue might have been opened anyway.
* Opening the issue waits on GitHub, so it's sent in the background while
* the button spins (see WritesInBackground), with the app and device
* details the web sends about its browser (see IssueEnvironment). Once it's
* open the draft goes, with any photos it had, and the sheet says "Opened
* issue #123." with View issue, as the web's toast does, or a toast does,
* with View issue as its button, if the sheet was swiped away by then (see
* SyncsOutbox::toastActionPressed()). When it isn't, the draft stays an
* issue, with why under it: devnull's own words when it sends them, else
* the web's, which say to check upstream when the issue might have been
* opened anyway.
*/
trait DraftsIssues
{
Expand Down Expand Up @@ -134,7 +137,7 @@ public function openIssueFromSheet(): void
$this->openingIssue = true;
$this->sheetError = null;

$this->writeInBackground('POST', Client::IssuesPath, ['command' => $command->command(), 'body' => $body], function (array $json): void {
$this->writeInBackground('POST', Client::IssuesPath, ['command' => $command->command(), 'body' => $body, 'environment' => IssueEnvironment::collect()], function (array $json): void {
$number = is_int($json['number'] ?? null) ? $json['number'] : null;
$url = is_string($json['url'] ?? null) ? $json['url'] : null;
$stash = app(Stash::class);
Expand Down
5 changes: 3 additions & 2 deletions resources/views/native/partials/issue-badge.blade.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{{--
The web's IssueCommandBadge, over the field of a draft that's an issue: $command in a soft chip, whose X turns the
draft back into a commit, then where the issue goes. The X's target is the chip's full height.
draft back into a commit, then where the issue goes and what goes with it (see IssueEnvironment). That line wraps
rather than truncating, so the device details it mentions are never cut off. The X's target is the chip's full height.
--}}
<native:row ref="issue-badge" class="w-full gap-2 items-center">
<native:row class="gap-[6] items-center pl-2 rounded-md bg-theme-primary-soft">
Expand All @@ -12,5 +13,5 @@
</native:column>
</native:pressable>
</native:row>
<native:text class="flex-1 text-[13px] text-theme-ink-muted" :max-lines="1">Public issue on {{ \App\Devnull\IssueCommand::Repository }}, crediting you</native:text>
<native:text class="flex-1 text-[13px] text-theme-ink-muted">Public issue on {{ \App\Devnull\IssueCommand::Repository }}, crediting you, with your app and device details</native:text>
</native:row>
51 changes: 44 additions & 7 deletions tests/Feature/Screens/IssueCommandsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@
use App\Devnull\Client;
use App\Devnull\Data\StashedDraft;
use App\Devnull\IssueCommand;
use App\Devnull\IssueEnvironment;
use App\Devnull\Stash;
use App\NativeComponents\Log;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Native\Mobile\Facades\System;
use Native\Mobile\Testing\Native;
use Native\Mobile\Testing\TestableComponent;
use Victorycodedev\ToastKit\Events\ToastActionPressed;
Expand All @@ -29,7 +31,7 @@ function issueSheetOnLog(?array $answer = null): TestableComponent
{
serveApi([
'GET /api/v1/log/following' => [pageJson([commitJson(['id' => 'c1', 'body' => 'An older commit', 'author' => userJson(['id' => 'u2', 'handle' => 'ada'])])])],
'POST /api/v1/issues' => $answer ?? [['number' => 123, 'url' => 'https://github.com/nunomaduro/dev-null.sh/issues/123'], 201],
'POST /api/v1/issues' => $answer ?? [['number' => 123, 'url' => 'https://github.com/dev-null-sh/dev-null.sh/issues/123'], 201],
]);

return Native::visit('/log', platform: 'ios')->press('composeInSheet');
Expand All @@ -53,7 +55,10 @@ function hasRef(TestableComponent $screen, string $ref): bool
->and(hasRef($screen, 'sheet-open-issue'))->toBeTrue()
->and(hasRef($screen, 'sheet-commit'))->toBeFalse();

$screen->assertSee($command)->assertSee('Public issue on nunomaduro/dev-null.sh, crediting you');
$screen->assertSee($command)->assertSee('Public issue on dev-null-sh/dev-null.sh, crediting you, with your app and device details');

// It says what goes with the issue, so it wraps rather than being cut off.
expect(findNode(nodeByRef($screen, 'issue-badge'), fn (array $node): bool => str_starts_with($node['props']['text'] ?? '', 'Public issue on'))['props'])->not->toHaveKey('max_lines');
})->with([
'feature' => ['/feature Dark mode', '/feature', 'Dark mode', IssueCommand::Feature->placeholder()],
'bug' => ['/bug ', '/bug', '', IssueCommand::Bug->placeholder()],
Expand Down Expand Up @@ -184,7 +189,8 @@ function hasRef(TestableComponent $screen, string $ref): bool

Http::assertSent(fn (Request $request): bool => $request->method() === 'POST'
&& $request->url() === 'http://devnull.test/api/v1/'.Client::IssuesPath
&& $request->body() === '{"command":"/bug","body":"Search loses its filters\n\nSteps: select PHP."}'
&& $request['command'] === '/bug'
&& $request['body'] === "Search loses its filters\n\nSteps: select PHP."
&& $request->hasHeader('X-SSH-Signature'));

expect(issueRequestsSent())->toBe(1)
Expand All @@ -195,7 +201,7 @@ function hasRef(TestableComponent $screen, string $ref): bool
$bridge->assertNotCalled('ToastKit.Show');

$screen->tap('view-issue')
->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/nunomaduro/dev-null.sh/issues/123');
->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/dev-null-sh/dev-null.sh/issues/123');

$screen->tap('issue-done');
expect(nodeByRef($screen, 'sheet')['props']['visible'])->toBeFalse();
Expand All @@ -206,6 +212,37 @@ function hasRef(TestableComponent $screen, string $ref): bool
->and(hasRef($screen, 'sheet-commit'))->toBeTrue();
});

it('sends the app and device details the web sends about its browser, never the phone\'s name', function () {
config(['nativephp.version' => '1.4.0']);
Native::fakeBridge()->respondTo('Device.GetInfo', ['info' => json_encode([
'name' => "Ada's iPhone",
'model' => 'iPhone',
'operatingSystem' => 'iOS',
'osVersion' => '27.2',
'language' => 'en-GB',
])]);
System::rememberAppearance('dark');

try {
issueSheetOnLog()->input('sheet-body', '/bug Broken.')->tap('sheet-open-issue');
} finally {
System::rememberAppearance('light');
}

Http::assertSent(fn (Request $request): bool => str_ends_with($request->url(), '/api/v1/'.Client::IssuesPath)
&& $request['environment'] === ['browser' => 'devnull app 1.4.0', 'os' => 'iOS 27.2', 'language' => 'en-GB', 'color_scheme' => 'dark']
&& ! str_contains($request->body(), 'Ada'));
});

it('leaves out the details the phone does not give, and cuts the rest to what devnull takes', function () {
config(['nativephp.version' => str_repeat('9', 120)]);

issueSheetOnLog()->input('sheet-body', '/bug Broken.')->tap('sheet-open-issue');

Http::assertSent(fn (Request $request): bool => str_ends_with($request->url(), '/api/v1/'.Client::IssuesPath)
&& $request['environment'] === ['browser' => mb_substr('devnull app '.str_repeat('9', 120), 0, IssueEnvironment::MaxLength), 'color_scheme' => 'light']);
});

it('opens no issue link that is not a web page', function () {
$screen = issueSheetOnLog([['number' => 123, 'url' => 'javascript:alert(1)'], 201])->input('sheet-body', '/bug Broken.')->tap('sheet-open-issue');

Expand All @@ -222,13 +259,13 @@ function hasRef(TestableComponent $screen, string $ref): bool
->assertNativeCalled('ToastKit.Show', fn (array $params): bool => $params['message'] === 'Opened issue #123.'
&& $params['variant'] === 'success'
&& $params['action'] === ['id' => 'view-issue', 'label' => 'View issue']
&& $params['data'] === ['url' => 'https://github.com/nunomaduro/dev-null.sh/issues/123']);
&& $params['data'] === ['url' => 'https://github.com/dev-null-sh/dev-null.sh/issues/123']);

expect(app(Stash::class)->get($this->userId, 'commit')->body)->toBe('')
->and(nodeByRef($screen, 'sheet')['props']['visible'])->toBeFalse();

$screen->emitNative(ToastActionPressed::class, ['toastId' => 'toast-1', 'actionId' => 'view-issue', 'data' => ['url' => 'https://github.com/nunomaduro/dev-null.sh/issues/123']])
->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/nunomaduro/dev-null.sh/issues/123');
$screen->emitNative(ToastActionPressed::class, ['toastId' => 'toast-1', 'actionId' => 'view-issue', 'data' => ['url' => 'https://github.com/dev-null-sh/dev-null.sh/issues/123']])
->assertNativeCalled('Browser.OpenInApp', fn (array $params): bool => $params['url'] === 'https://github.com/dev-null-sh/dev-null.sh/issues/123');
});

it('keeps the issue draft with why when devnull or GitHub says no', function (array $answer, string $message) {
Expand Down
Loading