• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

thorstenalpers / CleanMyPosts / 15187939643

22 May 2025 01:30PM UTC coverage: 22.394% (-0.2%) from 22.575%
15187939643

push

github

thorstenalpers
Fix show the correct number of posts, after page loaded

86 of 372 branches covered (23.12%)

Branch coverage included in aggregate %.

0 of 14 new or added lines in 1 file covered. (0.0%)

4 existing lines in 1 file now uncovered.

277 of 1249 relevant lines covered (22.18%)

0.61 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

10.47
/src/UI/Services/XScriptService.cs
1
using System.Net;
2
using Ardalis.GuardClauses;
3
using CleanMyPosts.UI.Contracts.Services;
4
using CleanMyPosts.UI.Helpers;
5
using CleanMyPosts.UI.Models;
6
using Microsoft.Extensions.Logging;
7

8
namespace CleanMyPosts.UI.Services;
9

10
public class XScriptService(ILogger<XScriptService> logger, IWebViewHostService webViewHostService, IUserSettingsService userSettingsService) : IXScriptService
6✔
11
{
12
    private readonly ILogger<XScriptService> _logger = logger;
6✔
13
    private readonly IWebViewHostService _webViewHostService = webViewHostService;
6✔
14
    private readonly IUserSettingsService _userSettingsService = userSettingsService;
6✔
15
    private string _userName;
16

17
    public async Task ShowPostsAsync()
18
    {
19
        Guard.Against.Null(_userName);
1✔
20
        var searchQuery = $"from:{_userName}";
1✔
21
        var encodedQuery = WebUtility.UrlEncode(searchQuery);
1✔
22
        var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query");
1✔
23

24
        if (_webViewHostService.Source == url)
1!
25
        {
26
            _webViewHostService.Reload();
×
27
        }
28
        else
29
        {
30
            _webViewHostService.Source = url;
1✔
31
        }
32

33
        if (!await WaitForFullDocumentReadyAsync())
1!
34
        {
35
            _logger.LogWarning("Navigation to {Url} failed.", url);
×
36
        }
37

38
        _logger.LogInformation("Navigated to {Url}", url);
×
39
    }
×
40

41
    public async Task<int> DeletePostsAsync()
42
    {
43
        Guard.Against.Null(_userName);
×
44

45
        var searchQuery = $"from:{_userName}";
×
46
        var encodedQuery = WebUtility.UrlEncode(searchQuery);
×
47
        var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query");
×
48

49
        if (_webViewHostService.Source == url)
×
50
        {
51
            _webViewHostService.Reload();
×
52
        }
53
        else
54
        {
55
            _webViewHostService.Source = url;
×
56
        }
57
        if (!await WaitForFullDocumentReadyAsync())
×
58
        {
59
            _logger.LogWarning("Navigation to search page failed.");
×
60
            return 0;
×
61
        }
NEW
62
        var totalPosts = 0;
×
63
        var postNumber = 1;
×
64
        while (await PostsExistAsync())
×
65
        {
66
            var countBefore = await GetPostsCountAsync();
×
NEW
67
            if (postNumber == 1)
×
68
            {
NEW
69
                totalPosts = countBefore;
×
70
            }
71

UNCOV
72
            _logger.LogInformation("Found {Count} posts before deletion.", countBefore);
×
73

74
            try
75
            {
76
                _logger.LogInformation("Deleting post #{Number}...", postNumber);
×
77
                await DeleteSinglePostAsync();
×
78

79
                if (await WaitForPostDeletedAsync(countBefore))
×
80
                {
81
                    _logger.LogInformation("Post #{Number} deleted successfully.", postNumber);
×
82
                }
83
                else
84
                {
85
                    _logger.LogWarning("Post #{Number} was not deleted (DOM unchanged).", postNumber);
×
86
                    break;
×
87
                }
88
            }
×
89
            catch (Exception ex)
×
90
            {
91
                _logger.LogError(ex, "Error deleting post #{Number}.", postNumber);
×
92
                break;
×
93
            }
94
            postNumber++;
×
95
        }
96
        _webViewHostService.Reload();
×
97
        await WaitForFullDocumentReadyAsync();
×
98

99
        _logger.LogInformation("Deleted {TotalPosts} posts.", totalPosts);
×
100
        return totalPosts;
×
101
    }
×
102

103
    public async Task ShowLikesAsync()
104
    {
105
        Guard.Against.Null(_userName);
1✔
106

107
        var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/likes");
1✔
108

109
        if (_webViewHostService.Source == url)
1!
110
        {
111
            _webViewHostService.Reload();
×
112
        }
113
        else
114
        {
115
            _webViewHostService.Source = url;
1✔
116
        }
117

118
        if (!await WaitForFullDocumentReadyAsync())
1!
119
        {
120
            _logger.LogWarning("Navigation to {Url} failed.", url);
×
121
        }
122

123
        _logger.LogInformation("Navigated to {Url}", url);
×
124
    }
×
125

126
    public async Task<int> DeleteLikesAsync()
127
    {
128
        Guard.Against.Null(_userName);
×
129

130
        var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/likes");
×
131

132
        if (_webViewHostService.Source == url)
×
133
        {
134
            _webViewHostService.Reload();
×
135
        }
136
        else
137
        {
138
            _webViewHostService.Source = url;
×
139
        }
140
        if (!await WaitForFullDocumentReadyAsync())
×
141
        {
142
            _logger.LogWarning("Navigation to search page failed.");
×
143
            return 0;
×
144
        }
145

NEW
146
        int totalLikes = 0;
×
147

148
        var postNumber = 1;
×
149

UNCOV
150
        while (await LikesExistAsync())
×
151
        {
152
            var countBefore = await GetLikesCountAsync();
×
NEW
153
            if (postNumber == 1)
×
154
            {
NEW
155
                totalLikes = countBefore;
×
156
            }
157

UNCOV
158
            _logger.LogInformation("Found {Count} likes before deletion.", countBefore);
×
159

160
            try
161
            {
162
                _logger.LogInformation("Deleting like #{Number}...", postNumber);
×
163
                await DeleteSingleLikeAsync();
×
164

165
                if (await WaitForLikeDeletedAsync(countBefore))
×
166
                {
167
                    _logger.LogInformation("Like #{Number} deleted successfully.", postNumber);
×
168
                }
169
                else
170
                {
171
                    _logger.LogWarning("Like #{Number} was not deleted (DOM unchanged).", postNumber);
×
172
                    break;
×
173
                }
174
            }
×
175
            catch (Exception ex)
×
176
            {
177
                _logger.LogError(ex, "Error deleting like #{Number}.", postNumber);
×
178
                break;
×
179
            }
180
            postNumber++;
×
181
        }
182

183
        _webViewHostService.Reload();
×
184
        await WaitForFullDocumentReadyAsync();
×
185

186
        _logger.LogInformation("Deleted {TotalLikes} Likes.", totalLikes);
×
187
        return totalLikes;
×
188
    }
×
189

190
    public async Task ShowFollowingAsync()
191
    {
192
        Guard.Against.Null(_userName);
1✔
193

194
        var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/following");
1✔
195

196
        if (_webViewHostService.Source == url)
1!
197
        {
198
            _webViewHostService.Reload();
×
199
        }
200
        else
201
        {
202
            _webViewHostService.Source = url;
1✔
203
        }
204

205
        if (!await WaitForFullDocumentReadyAsync())
1!
206
        {
207
            _logger.LogWarning("Navigation to {Url} failed.", url);
×
208
        }
209

210
        _logger.LogInformation("Navigated to {Url}", url);
×
211
    }
×
212

213
    public async Task<int> DeleteFollowingAsync()
214
    {
215
        Guard.Against.Null(_userName);
×
216

217
        var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/following");
×
218

219
        if (_webViewHostService.Source == url)
×
220
        {
221
            _webViewHostService.Reload();
×
222
        }
223
        else
224
        {
225
            _webViewHostService.Source = url;
×
226
        }
227
        if (!await WaitForFullDocumentReadyAsync())
×
228
        {
229
            _logger.LogWarning("Navigation to search page failed.");
×
230
            return 0;
×
231
        }
232

NEW
233
        int totalFollowings = 0;
×
234
        var postNumber = 1;
×
235
        while (await FollowingExistAsync())
×
236
        {
237
            var countBefore = await GetFollowingCountAsync();
×
NEW
238
            if (postNumber == 1)
×
239
            {
NEW
240
                totalFollowings = countBefore;
×
241
            }
242

UNCOV
243
            _logger.LogInformation("Found {Count} following before deletion.", countBefore);
×
244

245
            try
246
            {
247
                _logger.LogInformation("Deleting following #{Number}...", postNumber);
×
248
                await DeleteSingleFollowingAsync();
×
249

250
                if (await WaitForFollowingDeletedAsync(countBefore))
×
251
                {
252
                    _logger.LogInformation("Following #{Number} deleted successfully.", postNumber);
×
253
                }
254
                else
255
                {
256
                    _logger.LogWarning("Following #{Number} was not deleted (DOM unchanged).", postNumber);
×
257
                    break;
×
258
                }
259
            }
×
260
            catch (Exception ex)
×
261
            {
262
                _logger.LogError(ex, "Error deleting following #{Number}.", postNumber);
×
263
                break;
×
264
            }
265
            postNumber++;
×
266
        }
267

268
        _webViewHostService.Reload();
×
269
        await WaitForFullDocumentReadyAsync();
×
270

271
        _logger.LogInformation("Deleted {TotalFollowings} Followings.", totalFollowings);
×
272
        return totalFollowings;
×
273
    }
×
274

275
    private async Task<bool> PostsExistAsync()
276
    {
277
        const string js = "document.querySelector('div[data-testid=\"primaryColumn\"] section button[data-testid=\"caret\"]') !== null";
278
        for (var i = 0; i < 5; i++)
14✔
279
        {
280
            if (await _webViewHostService.ExecuteScriptAsync(js) == "true")
6✔
281
            {
282
                return true;
1✔
283
            }
284
            await Task.Delay(500);
5✔
285
        }
286
        return false;
1✔
287
    }
2✔
288

289
    private async Task<bool> FollowingExistAsync()
290
    {
291
        const string js = "document.querySelector('button[data-testid$=\"unfollow\"]') !== null";
292
        for (var i = 0; i < 5; i++)
×
293
        {
294
            if (await _webViewHostService.ExecuteScriptAsync(js) == "true")
×
295
            {
296
                return true;
×
297
            }
298
            await Task.Delay(500);
×
299
        }
300
        return false;
×
301
    }
×
302

303
    private async Task<bool> LikesExistAsync()
304
    {
305
        const string js = "document.querySelector('button[data-testid=\"unlike\"]') !== null";
306
        for (var i = 0; i < 5; i++)
×
307
        {
308
            if (await _webViewHostService.ExecuteScriptAsync(js) == "true")
×
309
            {
310
                return true;
×
311
            }
312
            await Task.Delay(500);
×
313
        }
314
        return false;
×
315
    }
×
316

317
    private async Task DeleteSinglePostAsync()
318
    {
319
        var waitBeforeTryClickDelete = _userSettingsService.GetSetting("WaitBeforeTryClickDelete", 10);
×
320
        var waitBetweenTryClickDeleteAttempts = _userSettingsService.GetSetting("WaitBetweenTryClickDeleteAttempts", 100);
×
321

322
        var js = $@"
×
323
        (() => {{
×
324
            const caret = document.querySelector(""div[data-testid='primaryColumn'] section button[data-testid='caret']"");
×
325
            if (!caret) return;
×
326

×
327
            caret.click();
×
328

×
329
            setTimeout(() => {{
×
330
                const delays = [ {waitBetweenTryClickDeleteAttempts}, 
×
331
                                {waitBetweenTryClickDeleteAttempts * 2}, 
×
332
                                {waitBetweenTryClickDeleteAttempts * 3}, 
×
333
                                {waitBetweenTryClickDeleteAttempts * 4}, 
×
334
                                {waitBetweenTryClickDeleteAttempts * 5} ];
×
335

×
336
                function tryClickDelete(attempt = 0) {{
×
337
                    if (attempt >= delays.length) return;
×
338
                    setTimeout(() => {{
×
339
                        const menu = document.querySelector(""[role='menu']"");
×
340
                        if (menu && menu.style.display !== ""none"") {{
×
341
                            const items = document.querySelectorAll(""[role='menuitem']"");
×
342
                            for (const item of items) {{
×
343
                                const span = item.querySelector(""span"");
×
344
                                if (!span) continue;
×
345

×
346
                                const color = getComputedStyle(span).color;
×
347
                                const rgb = color.match(/\d+/g).map(Number);
×
348
                                const [r, g, b] = rgb;
×
349
                                if (r > 180 && g < 100 && b < 100) {{
×
350
                                    span.click();
×
351
                                    confirmDelete();
×
352
                                    return;
×
353
                                }}
×
354
                            }}
×
355
                            tryClickDelete(attempt + 1);
×
356
                        }} else {{
×
357
                            tryClickDelete(attempt + 1);
×
358
                        }}
×
359
                    }}, delays[attempt]);
×
360
                }}
×
361

×
362
                function confirmDelete(attempt = 0) {{
×
363
                    if (attempt >= delays.length) return;
×
364
                    setTimeout(() => {{
×
365
                        const confirmBtn = document.querySelector(""button[data-testid='confirmationSheetConfirm']"");
×
366
                        if (confirmBtn && confirmBtn.offsetParent !== null) {{
×
367
                            confirmBtn.click();
×
368
                            window.scrollBy(0, 300);
×
369
                        }} else {{
×
370
                            confirmDelete(attempt + 1);
×
371
                        }}
×
372
                    }}, delays[attempt]);
×
373
                }}
×
374

×
375
                tryClickDelete();
×
376
            }}, {waitBeforeTryClickDelete});
×
377
        }})();";
×
378

379
        await _webViewHostService.ExecuteScriptAsync(js);
×
380
    }
×
381
    private async Task DeleteSingleLikeAsync()
382
    {
383
        var waitBeforeTryClickDelete = _userSettingsService.GetSetting<int>("WaitBeforeTryClickDelete");
×
384

385
        var js = $@"
×
386
        (() => {{
×
387
            const unlikeButton = document.querySelector('button[data-testid=""unlike""]');
×
NEW
388
            if (unlikeButton) {{
×
NEW
389
                unlikeButton.click();
×
NEW
390
            }}
×
NEW
391
            window.scrollBy(0, 300);
×
392
        }})();";
×
393
        await Task.Delay(waitBeforeTryClickDelete);
×
394
        await _webViewHostService.ExecuteScriptAsync(js);
×
395
    }
×
396

397
    private async Task DeleteSingleFollowingAsync()
398
    {
399
        var waitBeforeTryClickDelete = _userSettingsService.GetSetting<int>("WaitBeforeTryClickDelete");
×
400
        var waitBetweenTryClickDeleteAttempts = _userSettingsService.GetSetting<int>("WaitBetweenTryClickDeleteAttempts");
×
401

402
        var js = $@"
×
403
    (() => {{
×
404
        const unfollowingButton = document.querySelector('button[data-testid$=""-unfollow""]');
×
405
        if (!unfollowingButton) return;
×
406

×
407
        unfollowingButton.click();
×
408

×
409
        const delays = [
×
410
            {waitBetweenTryClickDeleteAttempts},
×
411
            {waitBetweenTryClickDeleteAttempts * 2},
×
412
            {waitBetweenTryClickDeleteAttempts * 3},
×
413
            {waitBetweenTryClickDeleteAttempts * 4},
×
414
            {waitBetweenTryClickDeleteAttempts * 5}
×
415
        ];
×
416

×
417
        function tryClickConfirm(attempt = 0) {{
×
418
            if (attempt >= delays.length) return;
×
419
            setTimeout(() => {{
×
420
                const confirmBtn = document.querySelector('button[data-testid=""confirmationSheetConfirm""]');
×
421
                if (confirmBtn && confirmBtn.offsetParent !== null) {{
×
422
                    confirmBtn.click();
×
423
                    window.scrollBy(0, 300);
×
424
                }} else {{
×
425
                    tryClickConfirm(attempt + 1);
×
426
                }}
×
427
            }}, delays[attempt]);
×
428
        }}
×
429

×
430
        setTimeout(() => {{
×
431
            tryClickConfirm();
×
432
        }}, {waitBeforeTryClickDelete});
×
433
    }})();";
×
434

435
        await _webViewHostService.ExecuteScriptAsync(js);
×
436
    }
×
437

438

439
    private async Task<int> GetPostsCountAsync()
440
    {
441
        const string js = """
442
        (() => document.querySelectorAll('div[data-testid="primaryColumn"] section button[data-testid="caret"]').length)()
443
        """;
444

445
        var result = await _webViewHostService.ExecuteScriptAsync(js);
×
446
        return int.TryParse(result?.Trim('"'), out var count) ? count : 0;
×
447
    }
×
448

449
    private async Task<int> GetFollowingCountAsync()
450
    {
451
        const string js = """
452
        (() => document.querySelectorAll('button[data-testid$="-unfollow"]').length)()        
453
        """;
454

455
        var result = await _webViewHostService.ExecuteScriptAsync(js);
×
456
        return int.TryParse(result?.Trim('"'), out var count) ? count : 0;
×
457
    }
×
458

459
    private async Task<int> GetLikesCountAsync()
460
    {
461
        const string js = """
462
        (() => document.querySelectorAll('button[data-testid="unlike"]').length)()
463
        """;
464

465
        var result = await _webViewHostService.ExecuteScriptAsync(js);
×
466
        return int.TryParse(result?.Trim('"'), out var count) ? count : 0;
×
467
    }
×
468

469
    private async Task<bool> WaitForPostDeletedAsync(int beforeCount)
470
    {
471
        int elapsed = 0, interval = 200, timeout = 5000;
×
472
        while (elapsed < timeout)
×
473
        {
474
            if (await GetPostsCountAsync() < beforeCount)
×
475
            {
476
                return true;
×
477
            }
478
            await Task.Delay(interval);
×
479
            elapsed += interval;
×
480
        }
481
        return false;
×
482
    }
×
483

484
    private async Task<bool> WaitForFollowingDeletedAsync(int beforeCount)
485
    {
486
        int elapsed = 0, interval = 200, timeout = 5000;
×
487
        while (elapsed < timeout)
×
488
        {
489
            if (await GetFollowingCountAsync() < beforeCount)
×
490
            {
491
                return true;
×
492
            }
493
            await Task.Delay(interval);
×
494
            elapsed += interval;
×
495
        }
496
        return false;
×
497
    }
×
498

499
    private async Task<bool> WaitForLikeDeletedAsync(int beforeCount)
500
    {
501
        int elapsed = 0, interval = 200, timeout = 5000;
×
502
        while (elapsed < timeout)
×
503
        {
504
            if (await GetLikesCountAsync() < beforeCount)
×
505
            {
506
                return true;
×
507
            }
508
            await Task.Delay(interval);
×
509
            elapsed += interval;
×
510
        }
511
        return false;
×
512
    }
×
513

514
    public async Task<string> GetUserNameAsync()
515
    {
516
        const string jsScript = @"
517
        (() => { 
518
            const el = document.querySelector('a[data-testid=""AppTabBar_Profile_Link""]');
519
            const href = el?.getAttribute('href');
520
            return href?.split('/')[1] ?? '';
521
        })()";
522
        var userName = await _webViewHostService.ExecuteScriptAsync(jsScript);
1✔
523
        _userName = Helper.CleanJsonResult(userName);
1✔
524
        return _userName;
1✔
525
    }
1✔
526

527
    private Task<bool> WaitForNavigationAsync()
528
    {
529
        var tcs = new TaskCompletionSource<bool>();
3✔
530

531
        async void Handler(object s, NavigationCompletedEventArgs e)
532
        {
533
            _webViewHostService.NavigationCompleted -= Handler;
×
534
            tcs.SetResult(e.IsSuccess);
×
535
            await Task.Delay(TimeSpan.FromMilliseconds(300));
×
536
        }
×
537
        _webViewHostService.NavigationCompleted += Handler;
3✔
538
        return tcs.Task;
3✔
539
    }
540

541
    private async Task<bool> WaitForFullDocumentReadyAsync()
542
    {
543
        if (!await WaitForNavigationAsync())
3!
544
        {
545
            return false;
×
546
        }
547

548
        const int maxAttempts = 50;
549
        const int delayMs = 100;
550

551
        for (var i = 0; i < maxAttempts; i++)
×
552
        {
553
            try
554
            {
555
                var readyStateJson = await _webViewHostService.ExecuteScriptAsync("document.readyState");
×
556
                var readyState = readyStateJson?.Trim('"').ToLowerInvariant();
×
557

558
                if (readyState == "complete")
×
559
                {
560
                    await Task.Delay(TimeSpan.FromMilliseconds(500));
×
561
                    return true;
×
562
                }
563
            }
×
564
            catch (Exception ex)
×
565
            {
566
                _logger.LogWarning(ex, "Error checking document.readyState.");
×
567
            }
×
568
            await Task.Delay(delayMs);
×
569
        }
NEW
570
        await Task.Delay(TimeSpan.FromMilliseconds(200));
×
571

572
        _logger.LogWarning("Timed out waiting for document.readyState = complete.");
×
573
        return false;
×
574
    }
×
575
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc