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

thorstenalpers / CleanMyPosts / 15141113493

20 May 2025 03:04PM UTC coverage: 0.0% (-11.5%) from 11.466%
15141113493

push

github

thorstenalpers
Change test framework

0 of 278 branches covered (0.0%)

Branch coverage included in aggregate %.

0 of 882 relevant lines covered (0.0%)

0.0 hits per line

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

0.0
/src/UI/Services/XWebViewScriptService.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 XWebViewScriptService(ILogger<XWebViewScriptService> logger, IWebViewHostService webViewHostService) : IXWebViewScriptService
×
11
{
12
    private readonly ILogger<XWebViewScriptService> _logger = logger;
×
13
    private readonly IWebViewHostService _webViewHostService = webViewHostService;
×
14
    private string _userName;
15

16
    public async Task ShowPostsAsync()
17
    {
18
        Guard.Against.Null(_userName);
×
19

20
        var searchQuery = $"from:{_userName}";
×
21
        var encodedQuery = WebUtility.UrlEncode(searchQuery);
×
22
        var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query");
×
23

24
        if (_webViewHostService.Source != url)
×
25
        {
26
            _webViewHostService.Source = url;
×
27
            if (!await WaitForFullDocumentReadyAsync())
×
28
            {
29
                _logger.LogWarning("Navigation to {Url} failed.", url);
×
30
            }
31
        }
32
    }
×
33

34
    public async Task DeletePostsAsync()
35
    {
36
        Guard.Against.Null(_userName);
×
37

38
        var searchQuery = $"from:{_userName}";
×
39
        var encodedQuery = WebUtility.UrlEncode(searchQuery);
×
40
        var url = new Uri($"https://x.com/search?q={encodedQuery}&src=typed_query");
×
41

42
        if (_webViewHostService.Source != url)
×
43
        {
44
            _webViewHostService.Source = url;
×
45
            if (!await WaitForFullDocumentReadyAsync())
×
46
            {
47
                _logger.LogWarning("Navigation to search page failed.");
×
48
                return;
×
49
            }
50
        }
51
        var postNumber = 1;
×
52
        while (await PostsExistAsync())
×
53
        {
54
            var countBefore = await GetCaretCountAsync();
×
55
            _logger.LogInformation("Found {Count} posts before deletion.", countBefore);
×
56

57
            try
58
            {
59
                _logger.LogInformation("Deleting post #{Number}...", postNumber);
×
60
                await DeleteSinglePostAsync();
×
61

62
                if (await WaitForPostDeletedAsync(countBefore))
×
63
                {
64
                    _logger.LogInformation("Post #{Number} deleted successfully.", postNumber);
×
65
                }
66
                else
67
                {
68
                    _logger.LogWarning("Post #{Number} was not deleted (DOM unchanged).", postNumber);
×
69
                    break;
×
70
                }
71
            }
×
72
            catch (Exception ex)
×
73
            {
74
                _logger.LogError(ex, "Error deleting post #{Number}.", postNumber);
×
75
                break;
×
76
            }
77
            postNumber++;
×
78
        }
79
        _logger.LogInformation("No more posts found.");
×
80
    }
×
81

82

83
    public async Task ShowLikesAsync()
84
    {
85
        Guard.Against.Null(_userName);
×
86

87
        var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/likes");
×
88
        if (_webViewHostService.Source != url)
×
89
        {
90
            _webViewHostService.Source = url;
×
91
            if (!await WaitForFullDocumentReadyAsync())
×
92
            {
93
                _logger.LogWarning("Navigation to {Url} failed.", url);
×
94
            }
95
        }
96
    }
×
97

98
    public Task DeleteStarredAsync()
99
    {
100
        throw new NotImplementedException();
×
101
    }
102

103
    public async Task ShowFollowingAsync()
104
    {
105
        Guard.Against.Null(_userName);
×
106

107
        var url = new Uri($"https://x.com/{WebUtility.UrlEncode(_userName)}/following");
×
108
        if (_webViewHostService.Source != url)
×
109
        {
110
            _webViewHostService.Source = url;
×
111
            if (!await WaitForFullDocumentReadyAsync())
×
112
            {
113
                _logger.LogWarning("Navigation to {Url} failed.", url);
×
114
            }
115
        }
116
    }
×
117

118
    public Task DeleteFollowingAsync()
119
    {
120
        throw new NotImplementedException();
×
121
    }
122

123
    private async Task<bool> PostsExistAsync()
124
    {
125
        const string js = "document.querySelector('div[data-testid=\"primaryColumn\"] section button[data-testid=\"caret\"]') !== null";
126
        for (var i = 0; i < 5; i++)
×
127
        {
128
            if (await _webViewHostService.ExecuteScriptAsync(js) == "true")
×
129
            {
130
                return true;
×
131
            }
132
            await Task.Delay(500);
×
133
        }
134
        return false;
×
135
    }
×
136

137
    private async Task DeleteSinglePostAsync()
138
    {
139
        const string js = """
140
        (() => {
141
            const caret = document.querySelector('div[data-testid="primaryColumn"] section button[data-testid="caret"]');
142
            if (!caret) return;
143

144
            caret.click();
145

146
            const delays = [10, 100, 200, 500, 1000];
147

148
            function tryClickDelete(attempt = 0) {
149
                if (attempt >= delays.length) return;
150
                setTimeout(() => {
151
                    const menu = document.querySelector('[role="menu"]');
152
                    if (menu && menu.style.display !== 'none') {
153
                        const items = document.querySelectorAll('[role="menuitem"]');
154
                        for (const item of items) {
155
                            const span = item.querySelector('span');
156
                            if (!span) continue;
157

158
                            const color = getComputedStyle(span).color;
159
                            const [r, g, b] = color.match(/\d+/g).map(Number);
160
                            if (r > 180 && g < 100 && b < 100) {
161
                                span.click();
162
                                confirmDelete();
163
                                return;
164
                            }
165
                        }
166
                        tryClickDelete(attempt + 1);
167
                    } else {
168
                        tryClickDelete(attempt + 1);
169
                    }
170
                }, delays[attempt]);
171
            }
172

173
            function confirmDelete(attempt = 0) {
174
                if (attempt >= delays.length) return;
175
                setTimeout(() => {
176
                    const confirmBtn = document.querySelector('button[data-testid="confirmationSheetConfirm"]');
177
                    if (confirmBtn && confirmBtn.offsetParent !== null) {
178
                        confirmBtn.click();
179
                        window.scrollBy(0, 300);
180
                    } else {
181
                        confirmDelete(attempt + 1);
182
                    }
183
                }, delays[attempt]);
184
            }
185

186
            tryClickDelete();
187
        })();
188
        """;
189
        await _webViewHostService.ExecuteScriptAsync(js);
×
190
    }
×
191

192
    private async Task<int> GetCaretCountAsync()
193
    {
194
        const string js = """
195
        (() => document.querySelectorAll('div[data-testid="primaryColumn"] section button[data-testid="caret"]').length)()
196
        """;
197

198
        var result = await _webViewHostService.ExecuteScriptAsync(js);
×
199
        return int.TryParse(result?.Trim('"'), out var count) ? count : 0;
×
200
    }
×
201

202
    private async Task<bool> WaitForPostDeletedAsync(int beforeCount)
203
    {
204
        int elapsed = 0, interval = 200, timeout = 5000;
×
205
        while (elapsed < timeout)
×
206
        {
207
            if (await GetCaretCountAsync() < beforeCount)
×
208
            {
209
                return true;
×
210
            }
211
            await Task.Delay(interval);
×
212
            elapsed += interval;
×
213
        }
214
        return false;
×
215
    }
×
216

217
    public async Task<string> GetUserNameAsync()
218
    {
219
        const string jsScript = @"
220
        (() => { 
221
            const el = document.querySelector('a[data-testid=""AppTabBar_Profile_Link""]');
222
            const href = el?.getAttribute('href');
223
            return href?.split('/')[1] ?? '';
224
        })()";
225
        var userName = await _webViewHostService.ExecuteScriptAsync(jsScript);
×
226
        _userName = Helper.CleanJsonResult(userName);
×
227
        return _userName;
×
228
    }
×
229

230
    private Task<bool> WaitForNavigationAsync()
231
    {
232
        var tcs = new TaskCompletionSource<bool>();
×
233

234
        async void Handler(object s, NavigationCompletedEventArgs e)
235
        {
236
            _webViewHostService.NavigationCompleted -= Handler;
×
237
            tcs.SetResult(e.IsSuccess);
×
238
            await Task.Delay(TimeSpan.FromMilliseconds(300));
×
239
        }
×
240
        _webViewHostService.NavigationCompleted += Handler;
×
241
        return tcs.Task;
×
242
    }
243

244
    private async Task<bool> WaitForFullDocumentReadyAsync()
245
    {
246
        if (!await WaitForNavigationAsync())
×
247
        {
248
            return false;
×
249
        }
250

251
        const int maxAttempts = 50;
252
        const int delayMs = 100;
253

254
        for (var i = 0; i < maxAttempts; i++)
×
255
        {
256
            try
257
            {
258
                var readyStateJson = await _webViewHostService.ExecuteScriptAsync("document.readyState");
×
259
                var readyState = readyStateJson?.Trim('"').ToLowerInvariant();
×
260

261
                if (readyState == "complete")
×
262
                {
263
                    await Task.Delay(TimeSpan.FromMilliseconds(500));
×
264
                    return true;
×
265
                }
266
            }
×
267
            catch (Exception ex)
×
268
            {
269
                _logger.LogWarning(ex, "Error checking document.readyState.");
×
270
            }
×
271
            await Task.Delay(delayMs);
×
272
        }
273
        _logger.LogWarning("Timed out waiting for document.readyState = complete.");
×
274
        return false;
×
275
    }
×
276
}
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