diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 0000000..7aee387 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,33 @@ +name: Build and test + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + build-and-test: + runs-on: windows-latest + timeout-minutes: 10 + + steps: + - name: Check out source + uses: actions/checkout@v6 + + - name: Set up .NET 8 + uses: actions/setup-dotnet@v5 + with: + dotnet-version: 8.0.x + + - name: Restore dependencies + run: dotnet restore WorkNotes.sln + + - name: Build release configuration + run: dotnet build WorkNotes.sln --configuration Release --no-restore + + - name: Run tests + run: dotnet test WorkNotes.sln --configuration Release --no-build --no-restore --verbosity normal diff --git a/WorkNotes.Tests/LinkTextParserTests.cs b/WorkNotes.Tests/LinkTextParserTests.cs new file mode 100644 index 0000000..ce45eee --- /dev/null +++ b/WorkNotes.Tests/LinkTextParserTests.cs @@ -0,0 +1,89 @@ +using WorkNotes.Services; + +namespace WorkNotes.Tests; + +public sealed class LinkTextParserTests +{ + [Fact] + public void FindLinks_ReturnsNoLinksForEmptyText() + { + Assert.Empty(LinkTextParser.FindLinks(string.Empty)); + } + + [Fact] + public void FindLinks_DetectsEmailWithoutOverlappingDomain() + { + const string text = "Contact dev@example.com for help."; + + var link = Assert.Single(LinkTextParser.FindLinks(text)); + + Assert.Equal("dev@example.com", link.DisplayText); + Assert.Equal("mailto:dev@example.com", link.Url); + Assert.Equal(text.IndexOf("dev@example.com", StringComparison.Ordinal), link.StartOffset); + Assert.Equal(link.StartOffset + link.DisplayText.Length, link.EndOffset); + } + + [Theory] + [InlineData("https://example.com/docs", "https://example.com/docs")] + [InlineData("http://example.com", "http://example.com")] + [InlineData("www.example.com/path", "https://www.example.com/path")] + [InlineData("example.com/path?q=1#top", "https://example.com/path?q=1#top")] + public void FindLinks_NormalizesSupportedUrlForms(string text, string expectedUrl) + { + var link = Assert.Single(LinkTextParser.FindLinks(text)); + + Assert.Equal(text, link.DisplayText); + Assert.Equal(expectedUrl, link.Url); + } + + [Theory] + [InlineData("Visit example.com.", "example.com")] + [InlineData("Visit example.com, then continue", "example.com")] + [InlineData("Visit (example.com)", "example.com")] + [InlineData("Visit [example.com]", "example.com")] + public void FindLinks_TrimsSentencePunctuation(string text, string expectedDisplayText) + { + var link = Assert.Single(LinkTextParser.FindLinks(text)); + + Assert.Equal(expectedDisplayText, link.DisplayText); + Assert.Equal("https://" + expectedDisplayText, link.Url); + } + + [Fact] + public void FindLinks_PreservesBalancedParenthesesInUrl() + { + const string text = "https://example.com/wiki/Function_(mathematics)"; + + var link = Assert.Single(LinkTextParser.FindLinks(text)); + + Assert.Equal(text, link.DisplayText); + Assert.Equal(text.Length, link.EndOffset); + } + + [Fact] + public void FindLinks_AppliesBaseOffsetToExclusiveRange() + { + const string text = "example.com"; + + var link = Assert.Single(LinkTextParser.FindLinks(text, baseOffset: 17)); + + Assert.Equal(17, link.StartOffset); + Assert.Equal(17 + text.Length, link.EndOffset); + } + + [Fact] + public void FindLinks_ReturnsMixedLinksInDocumentOrder() + { + const string text = "See example.com or email team@example.org, then visit https://docs.example.net/start."; + + var links = LinkTextParser.FindLinks(text); + + Assert.Collection( + links, + link => Assert.Equal("example.com", link.DisplayText), + link => Assert.Equal("team@example.org", link.DisplayText), + link => Assert.Equal("https://docs.example.net/start", link.DisplayText)); + Assert.True(links[0].EndOffset <= links[1].StartOffset); + Assert.True(links[1].EndOffset <= links[2].StartOffset); + } +} diff --git a/WorkNotes.Tests/WorkNotes.Tests.csproj b/WorkNotes.Tests/WorkNotes.Tests.csproj new file mode 100644 index 0000000..f09b6d0 --- /dev/null +++ b/WorkNotes.Tests/WorkNotes.Tests.csproj @@ -0,0 +1,29 @@ + + + + net8.0-windows + Exe + enable + enable + false + true + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + + diff --git a/WorkNotes.sln b/WorkNotes.sln index 68e4aee..9284638 100644 --- a/WorkNotes.sln +++ b/WorkNotes.sln @@ -1,19 +1,25 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.0.31903.59 -MinimumVisualStudioVersion = 10.0.40219.1 + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkNotes", "WorkNotes\WorkNotes.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WorkNotes.Tests", "WorkNotes.Tests\WorkNotes.Tests.csproj", "{B0DA7728-F09E-4076-96F1-D37A0C653DBA}" +EndProject Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0DA7728-F09E-4076-96F1-D37A0C653DBA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection EndGlobal diff --git a/WorkNotes/WorkNotes.csproj b/WorkNotes/WorkNotes.csproj index d28bab0..4c06999 100644 --- a/WorkNotes/WorkNotes.csproj +++ b/WorkNotes/WorkNotes.csproj @@ -11,12 +11,16 @@ 1.5.0.0 - - - - - - + + + + + + + + + +