Skip to content

Commit 4bf27a5

Browse files
authored
Merge pull request #9 from linkdotnet/feature/give-kudos
Feature/give kudos
2 parents 662d3d5 + 2823dd0 commit 4bf27a5

File tree

9 files changed

+256
-1
lines changed

9 files changed

+256
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using System.Threading.Tasks;
2+
using Blazored.LocalStorage;
3+
using Blazored.Toast.Services;
4+
using Bunit;
5+
using Bunit.TestDoubles;
6+
using FluentAssertions;
7+
using LinkDotNet.Blog.TestUtilities;
8+
using LinkDotNet.Blog.Web.Pages;
9+
using LinkDotNet.Blog.Web.Shared;
10+
using LinkDotNet.Infrastructure.Persistence;
11+
using Microsoft.EntityFrameworkCore;
12+
using Microsoft.Extensions.DependencyInjection;
13+
using Moq;
14+
using Xunit;
15+
16+
namespace LinkDotNet.Blog.IntegrationTests.Web.Pages
17+
{
18+
public class BlogPostPageTests : SqlDatabaseTestBase
19+
{
20+
[Fact]
21+
public async Task ShouldAddLikeOnEvent()
22+
{
23+
var publishedPost = new BlogPostBuilder().WithLikes(2).IsPublished().Build();
24+
await BlogPostRepository.StoreAsync(publishedPost);
25+
using var ctx = new TestContext();
26+
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
27+
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
28+
ctx.Services.AddScoped(_ => new Mock<ILocalStorageService>().Object);
29+
ctx.Services.AddScoped(_ => new Mock<IToastService>().Object);
30+
ctx.AddTestAuthorization().SetAuthorized("s");
31+
var cut = ctx.RenderComponent<BlogPostPage>(
32+
p => p.Add(b => b.BlogPostId, publishedPost.Id));
33+
var likeComponent = cut.FindComponent<Like>();
34+
likeComponent.SetParametersAndRender(c => c.Add(p => p.BlogPost, publishedPost));
35+
36+
likeComponent.Find("button").Click();
37+
38+
var fromDb = await DbContext.BlogPosts.AsNoTracking().SingleAsync(d => d.Id == publishedPost.Id);
39+
fromDb.Likes.Should().Be(3);
40+
}
41+
42+
[Fact]
43+
public async Task ShouldSubtractLikeOnEvent()
44+
{
45+
var publishedPost = new BlogPostBuilder().WithLikes(2).IsPublished().Build();
46+
await BlogPostRepository.StoreAsync(publishedPost);
47+
using var ctx = new TestContext();
48+
var localStorage = new Mock<ILocalStorageService>();
49+
localStorage.Setup(l => l.ContainKeyAsync("hasLiked", default)).ReturnsAsync(true);
50+
localStorage.Setup(l => l.GetItemAsync<bool>("hasLiked", default)).ReturnsAsync(true);
51+
ctx.JSInterop.Mode = JSRuntimeMode.Loose;
52+
ctx.Services.AddScoped<IRepository>(_ => BlogPostRepository);
53+
ctx.Services.AddScoped(_ => localStorage.Object);
54+
ctx.Services.AddScoped(_ => new Mock<IToastService>().Object);
55+
ctx.AddTestAuthorization().SetAuthorized("s");
56+
var cut = ctx.RenderComponent<BlogPostPage>(
57+
p => p.Add(b => b.BlogPostId, publishedPost.Id));
58+
var likeComponent = cut.FindComponent<Like>();
59+
likeComponent.SetParametersAndRender(c => c.Add(p => p.BlogPost, publishedPost));
60+
61+
likeComponent.Find("button").Click();
62+
63+
var fromDb = await DbContext.BlogPosts.AsNoTracking().SingleAsync(d => d.Id == publishedPost.Id);
64+
fromDb.Likes.Should().Be(1);
65+
}
66+
}
67+
}

LinkDotNet.Blog.TestUtilities/BlogPostBuilder.cs

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public class BlogPostBuilder
1010
private string url = "localhost";
1111
private bool isPublished = true;
1212
private string[] tags;
13+
private int likes;
1314

1415
public BlogPostBuilder WithTitle(string title)
1516
{
@@ -47,9 +48,17 @@ public BlogPostBuilder IsPublished(bool isPublished = true)
4748
return this;
4849
}
4950

51+
public BlogPostBuilder WithLikes(int likes)
52+
{
53+
this.likes = likes;
54+
return this;
55+
}
56+
5057
public BlogPost Build()
5158
{
52-
return BlogPost.Create(title, shortDescription, content, url, isPublished, tags);
59+
var blogPost = BlogPost.Create(title, shortDescription, content, url, isPublished, tags);
60+
blogPost.Likes = likes;
61+
return blogPost;
5362
}
5463
}
5564
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
using Blazored.LocalStorage;
2+
using Bunit;
3+
using FluentAssertions;
4+
using LinkDotNet.Blog.TestUtilities;
5+
using LinkDotNet.Blog.Web.Shared;
6+
using Microsoft.Extensions.DependencyInjection;
7+
using Moq;
8+
using Xunit;
9+
10+
namespace LinkDotNet.Blog.UnitTests.Web.Shared
11+
{
12+
public class LikeTests : TestContext
13+
{
14+
[Theory]
15+
[InlineData(0, "0 Likes")]
16+
[InlineData(1, "1 Like")]
17+
[InlineData(2, "2 Likes")]
18+
public void ShouldDisplayLikes(int likes, string expectedText)
19+
{
20+
Services.AddScoped(_ => new Mock<ILocalStorageService>().Object);
21+
var blogPost = new BlogPostBuilder().WithLikes(likes).Build();
22+
var cut = RenderComponent<Like>(
23+
p => p.Add(l => l.BlogPost, blogPost));
24+
25+
var label = cut.Find("small").TextContent;
26+
27+
label.Should().Be(expectedText);
28+
}
29+
30+
[Fact]
31+
public void ShouldInvokeEventWhenButtonClicked()
32+
{
33+
Services.AddScoped(_ => new Mock<ILocalStorageService>().Object);
34+
var blogPost = new BlogPostBuilder().Build();
35+
var wasClicked = false;
36+
var wasLike = false;
37+
var cut = RenderComponent<Like>(
38+
p => p.Add(l => l.BlogPost, blogPost)
39+
.Add(l => l.OnBlogPostLiked, b =>
40+
{
41+
wasClicked = true;
42+
wasLike = b;
43+
}));
44+
45+
cut.Find("button").Click();
46+
47+
wasClicked.Should().BeTrue();
48+
wasLike.Should().BeTrue();
49+
}
50+
51+
[Fact]
52+
public void ShouldSetLocalStorageVariableOnClick()
53+
{
54+
var localStorage = new Mock<ILocalStorageService>();
55+
Services.AddScoped(_ => localStorage.Object);
56+
var blogPost = new BlogPostBuilder().Build();
57+
var cut = RenderComponent<Like>(
58+
p => p.Add(l => l.BlogPost, blogPost));
59+
60+
cut.Find("button").Click();
61+
62+
localStorage.Verify(l => l.SetItemAsync("hasLiked", true, default), Times.Once);
63+
}
64+
65+
[Fact]
66+
public void ShouldCheckLocalStorageOnInit()
67+
{
68+
var localStorage = new Mock<ILocalStorageService>();
69+
localStorage.Setup(l => l.ContainKeyAsync("hasLiked", default)).ReturnsAsync(true);
70+
localStorage.Setup(l => l.GetItemAsync<bool>("hasLiked", default)).ReturnsAsync(true);
71+
Services.AddScoped(_ => localStorage.Object);
72+
var blogPost = new BlogPostBuilder().Build();
73+
var wasLike = true;
74+
var cut = RenderComponent<Like>(
75+
p => p.Add(l => l.BlogPost, blogPost)
76+
.Add(l => l.OnBlogPostLiked, b => wasLike = b));
77+
78+
cut.Find("button").Click();
79+
80+
wasLike.Should().BeFalse();
81+
}
82+
83+
[Fact]
84+
public void ShouldCheckStorageOnClickAgainAndDoNothingOnMismatch()
85+
{
86+
var localStorage = new Mock<ILocalStorageService>();
87+
Services.AddScoped(_ => localStorage.Object);
88+
var blogPost = new BlogPostBuilder().Build();
89+
var wasClicked = false;
90+
var cut = RenderComponent<Like>(
91+
p => p.Add(l => l.BlogPost, blogPost)
92+
.Add(l => l.OnBlogPostLiked, _ => wasClicked = true));
93+
localStorage.Setup(l => l.ContainKeyAsync("hasLiked", default)).ReturnsAsync(true);
94+
localStorage.Setup(l => l.GetItemAsync<bool>("hasLiked", default)).ReturnsAsync(true);
95+
96+
cut.Find("button").Click();
97+
98+
wasClicked.Should().BeFalse();
99+
}
100+
}
101+
}

LinkDotNet.Blog.Web/LinkDotNet.Blog.Web.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9+
<PackageReference Include="Blazored.LocalStorage" Version="4.1.2" />
910
<PackageReference Include="Blazored.Toast" Version="3.1.2" />
1011
<PackageReference Include="Markdig" Version="0.25.0" />
1112
<PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="5.0.7" />

LinkDotNet.Blog.Web/Pages/BlogPostPage.razor

+8
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ else
3232
@(RenderMarkupString(BlogPost.Content))
3333
</div>
3434
</div>
35+
<Like BlogPost="@BlogPost" OnBlogPostLiked="@UpdateLikes"></Like>
3536
</div>
3637
</div>
3738
}
@@ -55,4 +56,11 @@ else
5556
StateHasChanged();
5657
}
5758
}
59+
60+
private async Task UpdateLikes(bool hasLiked)
61+
{
62+
BlogPost = await _repository.GetByIdAsync(BlogPostId);
63+
BlogPost.Likes = hasLiked ? BlogPost.Likes + 1 : BlogPost.Likes - 1;
64+
await _repository.StoreAsync(BlogPost);
65+
}
5866
}

LinkDotNet.Blog.Web/Shared/Like.razor

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@using LinkDotNet.Domain
2+
@using Blazored.LocalStorage
3+
@using LinkDotNet.Infrastructure.Persistence
4+
@inject ILocalStorageService _localStorage
5+
<div class="like-container">
6+
<small>@BlogPost.Likes @LikeText</small>
7+
<button class="btn @BtnClass" @onclick="LikeBlogPost"><i class="far fa-thumbs-up"></i> @LikeTextButton</button>
8+
</div>
9+
10+
@code {
11+
[Parameter]
12+
public BlogPost BlogPost { get; set; }
13+
14+
[Parameter]
15+
public EventCallback<bool> OnBlogPostLiked { get; set; }
16+
17+
private bool HasLiked { get; set; }
18+
19+
private string BtnClass => HasLiked ? "btn-secondary" : "btn-primary";
20+
21+
private string LikeTextButton => HasLiked ? "Unlike" : "Like";
22+
23+
private string LikeText => BlogPost.Likes == 1 ? "Like" : "Likes";
24+
25+
protected override async Task OnAfterRenderAsync(bool firstRender)
26+
{
27+
if (firstRender)
28+
{
29+
HasLiked = await GetHasLiked();
30+
StateHasChanged();
31+
}
32+
}
33+
34+
private async Task LikeBlogPost()
35+
{
36+
// Prevent multiple open sites to like / unlike multiple times
37+
var hasLikedFromLocalStorage = await GetHasLiked();
38+
if (HasLiked != hasLikedFromLocalStorage)
39+
{
40+
return;
41+
}
42+
43+
HasLiked = !HasLiked;
44+
await OnBlogPostLiked.InvokeAsync(HasLiked);
45+
await _localStorage.SetItemAsync("hasLiked", HasLiked);
46+
}
47+
48+
private async Task<bool> GetHasLiked()
49+
{
50+
if (await _localStorage.ContainKeyAsync("hasLiked"))
51+
{
52+
return await _localStorage.GetItemAsync<bool>("hasLiked");
53+
}
54+
55+
return false;
56+
}
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.like-container {
2+
float: right;
3+
margin-top: 20px;
4+
}
5+
6+
.like-container button {
7+
margin-left: 10px;
8+
}

LinkDotNet.Blog.Web/Startup.cs

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using Blazored.LocalStorage;
12
using Blazored.Toast;
23
using LinkDotNet.Blog.Web.Authentication.Auth0;
34
using LinkDotNet.Blog.Web.RegistrationExtensions;
@@ -39,6 +40,7 @@ public void ConfigureServices(IServiceCollection services)
3940
services.UseAuth0Authentication(Configuration);
4041

4142
services.AddBlazoredToast();
43+
services.AddBlazoredLocalStorage();
4244
}
4345

4446
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

LinkDotNet.Domain/BlogPost.cs

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ private BlogPost()
2626

2727
public bool IsPublished { get; set; }
2828

29+
public int Likes { get; set; }
30+
2931
public static BlogPost Create(
3032
string title,
3133
string shortDescription,

0 commit comments

Comments
 (0)