1<?php
2
3// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the GNU Affero General Public License v3.0.
4// See the LICENCE file in the repository root for full licence text.
5
6namespace Tests\Browser;
7
8use App\Models\Beatmap;
9use App\Models\BeatmapDiscussion;
10use App\Models\BeatmapDiscussionPost;
11use App\Models\Beatmapset;
12use App\Models\User;
13use Laravel\Dusk\Browser;
14use Tests\DuskTestCase;
15
16class BeatmapDiscussionPostsTest extends DuskTestCase
17{
18 private const NEW_REPLY_SELECTOR = '.beatmap-discussion-post--new-reply';
19 private const RESOLVE_BUTTON_SELECTOR = '.btn-osu-big[data-action=reply_resolve]';
20
21 private Beatmap $beatmap;
22 private BeatmapDiscussion $beatmapDiscussion;
23 private Beatmapset $beatmapset;
24 private User $mapper;
25 private User $user;
26
27 public function testConcurrentPostAfterResolve()
28 {
29 $this->browse(function (Browser $first, Browser $second) {
30 // Setup both browsers.
31 $this->visitDiscussionPageAsUser($first, $this->mapper);
32 $this->visitDiscussionPageAsUser($second, $this->user);
33
34 // Write a reply...
35 $this->writeReply($first, 'Fixed');
36 $this->writeReply($second, 'Hey!');
37
38 // And send the replies.
39 $this->postReply($first, 'resolve');
40 $this->postReply($second, 'reply');
41
42 $first->pause(2000);
43 $second->pause(2000);
44
45 $this->assertSame(true, $this->beatmapDiscussion->fresh()->resolved);
46 });
47 }
48
49 protected function writeReply(Browser $browser, $reply)
50 {
51 $browser->with(static::NEW_REPLY_SELECTOR, function (Browser $newReply) use ($reply) {
52 $newReply->press(trans('beatmap_discussions.reply.open.user'))
53 ->waitFor('textarea')
54 ->type('textarea', $reply);
55 });
56 }
57
58 protected function postReply(Browser $browser, $action)
59 {
60 $browser->with(static::NEW_REPLY_SELECTOR, function (Browser $newReply) use ($action) {
61 switch ($action) {
62 case 'resolve':
63 // button may be covered by dev banner;
64 // ->element->($selector)->getLocationOnScreenOnceScrolledIntoView() uses { block: 'end', inline: 'nearest' } which isn't enough.
65 $newReply->scrollIntoView(static::RESOLVE_BUTTON_SELECTOR);
66 $newReply->element(static::RESOLVE_BUTTON_SELECTOR)->click();
67 break;
68 default:
69 $newReply->keys('textarea', '{enter}');
70 break;
71 }
72 });
73 }
74
75 protected function visitDiscussionPageAsUser(Browser $browser, User $user): void
76 {
77 $browser->loginAs($user)
78 ->visit('/_dusk/verify')
79 ->visitRoute('beatmapsets.discussions.show', [
80 'discussion' => $this->beatmapDiscussion->getKey(),
81 ]);
82 }
83
84 protected function deleteUser(User $user): void
85 {
86 $user->userProfileCustomization()->forceDelete();
87 $user->forceDelete();
88 }
89
90 protected function cleanup(): void
91 {
92 // Delete all models we created.
93 $this->beatmapDiscussion->beatmapDiscussionPosts()->forceDelete();
94 $this->beatmapDiscussion->forceDelete();
95 $this->beatmap->forceDelete();
96 $this->beatmapset->events()->forceDelete();
97 $this->beatmapset->forceDelete();
98 $this->deleteUser($this->user);
99 $this->deleteUser($this->mapper);
100 }
101
102 protected function setUp(): void
103 {
104 parent::setUp();
105
106 $this->mapper = User::factory()->withPlays()->create();
107 $this->user = User::factory()->withPlays()->create();
108
109 $this->beatmapset = Beatmapset::factory()->create([
110 'user_id' => $this->mapper,
111 ]);
112 $this->beatmap = $this->beatmapset->beatmaps()->save(Beatmap::factory()->make([
113 'user_id' => $this->mapper,
114 ]));
115 $this->beatmapDiscussion = BeatmapDiscussion::factory()->timeline()->create([
116 'beatmapset_id' => $this->beatmapset,
117 'beatmap_id' => $this->beatmap,
118 'user_id' => $this->user,
119 ]);
120 $post = BeatmapDiscussionPost::factory()->timeline()->make([
121 'user_id' => $this->user,
122 ]);
123 $this->beatmapDiscussion->beatmapDiscussionPosts()->save($post);
124
125 $this->beforeApplicationDestroyed(function () {
126 // Similar case to SanityTest, cleanup the models we created during the test.
127 $this->cleanup();
128 });
129 }
130}