Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
# hn
Yet [another](https://github.com/donnemartin/haxor-news) CLI tool to browse the top posts on Hacker News using [their API](https://github.com/HackerNews/API). This project was just an excuse to brush up on golang and to get some more experience working with TUIs and the vscode debugger.

> <video src="https://github.com/dominickp/hn/assets/4555880/aab9d893-4ad0-413b-b926-4ef076ed7f0c" width=896>
<img src="./docs/top-menu.png">

<img src="./docs/comment-thread.png">

## Usage

Either grab a binary from a [release](https://github.com/dominickp/hn/releases) and add it to your PATH or clone this repo and run `go run .` or `go install`.

## Todo
- Testing
Binary file added docs/comment-thread.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/top-menu.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
133 changes: 133 additions & 0 deletions model_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package main

import (
"fmt"
"reflect"
"testing"

"github.com/dominickp/hn/client"
)

func Test_getTopMenuCurrentPageChoices(t *testing.T) {
type args struct {
m model
}

topMenuResponse := client.TopMenuResponse{}
numItems := 25
for i := 1; i < numItems; i++ {
item := client.Item{Id: i, Title: fmt.Sprintf("item %d", i), Score: 33}
topMenuResponse.Items = append(topMenuResponse.Items, item)
}

tests := []struct {
name string
args args
want []string
}{
{
name: "TestGetTopMenuCurrentPageChoices",
args: args{m: model{currentPage: 1, pageSize: 5, topMenuResponse: topMenuResponse}},
want: []string{"33 item 1", "33 item 2", "33 item 3", "33 item 4", "33 item 5"},
},
{
name: "TestGetTopMenuCurrentPageChoicesPage2",
args: args{m: model{currentPage: 2, pageSize: 5, topMenuResponse: topMenuResponse}},
want: []string{"33 item 6", "33 item 7", "33 item 8", "33 item 9", "33 item 10"},
},
{
name: "TestGetTopMenuCurrentPageLongerPage",
args: args{m: model{currentPage: 1, pageSize: 10, topMenuResponse: topMenuResponse}},
want: []string{
"33 item 1", "33 item 2", "33 item 3", "33 item 4", "33 item 5",
"33 item 6", "33 item 7", "33 item 8", "33 item 9", "33 item 10",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getTopMenuCurrentPageChoices(tt.args.m); !reflect.DeepEqual(got, tt.want) {
t.Errorf("getTopMenuCurrentPageChoices() = %v, want %v", got, tt.want)
}
})
}
}

func Test_model_getCurrentTopic(t *testing.T) {
tests := []struct {
name string
m model
want *client.Item
}{
{
name: "TestGetCurrentTopic",
m: model{topicHistoryStack: []client.Item{
{Id: 1, Title: "item 1"},
{Id: 1, Title: "item 2"},
{Id: 1, Title: "item 3"},
}},
want: &client.Item{Id: 1, Title: "item 3"},
},
{
name: "TestGetCurrentTopicEmptyStack",
m: model{topicHistoryStack: []client.Item{}},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.m.getCurrentTopic(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("model.getCurrentTopic() = %v, want %v", got, tt.want)
}
})
}
}

func Test_getContent(t *testing.T) {
type args struct {
m model
}
tests := []struct {
name string
args args
want string
}{
{
name: "TestGetContentWithCurrentTopic",
args: args{m: model{
topicHistoryStack: []client.Item{{
Id: 1, Title: "item 1", By: "Joe", Kids: []int{1, 2, 3}, Url: "http://example.com", Text: "foo...",
}},
choices: []string{"33 item 1", "33 item 2"},
}},
want: `item 1
By Joe (3 comments)
foo...
→ http://example.com

> 33 item 1
33 item 2
`,
},
{
name: "TestGetContentTopMenu",
args: args{m: model{
topicHistoryStack: []client.Item{},
choices: []string{"33 item 1", "33 item 2", "33 item 3", "33 item 4", "33 item 5"},
}},
want: `> 33 item 1
33 item 2
33 item 3
33 item 4
33 item 5
`,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := getContent(tt.args.m); got != tt.want {
t.Errorf("getContent() = \n'%v', want \n'%v'", got, tt.want)
}
})
}
}
2 changes: 1 addition & 1 deletion util/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func HtmlToText(s string) string {
}
if n.FirstChild != nil {
linkText := n.FirstChild.Data
styledText := LinkStyle.Render(linkText)
styledText := LinkStyle.Render(href)
if rel != "" {
s = strings.Replace(s, `<a href="`+href+`" rel="`+rel+`">`+linkText+`</a>`, styledText, -1)
} else {
Expand Down
119 changes: 119 additions & 0 deletions util/util_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package util

import (
"testing"
)

func TestPadRight(t *testing.T) {
type args struct {
str string
length int
}
tests := []struct {
name string
args args
want string
}{
{
name: "TestPadRight",
args: args{str: "hello", length: 10},
want: "hello ",
},
{
name: "TestLongerInput",
args: args{str: "hello world", length: 5},
want: "hello world",
},
{
name: "TestEmpty",
args: args{str: "", length: 5},
want: " ",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := PadRight(tt.args.str, tt.args.length); got != tt.want {
t.Errorf("PadRight() = '%v', want '%v'", got, tt.want)
}
})
}
}

func TestHtmlToText(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want string
}{
{
name: "TestReplaceP",
args: args{s: "Hello<p>world</p>"},
want: "Hello\nworld",
},
{
name: "TestReplaceMultipleP",
args: args{s: "Hello<p>world.</p> Say hello to my <p>little friend!</p>"},
want: "Hello\nworld.\n Say hello to my \nlittle friend!",
},
{
name: "TestItalicizeI",
args: args{s: "Hello <i>world</i>"},
want: "Hello world", // I'm not sure why ansi escape codes are not being rendered here
},
{
name: "TestLinkFormatting",
args: args{s: "This is a link: <a href=\"https://example.com\">example</a>"},
want: "This is a link: https://example.com",
},
{
name: "TestLinkFormattingWRel",
args: args{s: "This is a link: <a href=\"https://example.com\" rel=\"foo\">example</a>"},
want: "This is a link: https://example.com",
},
{
name: "TestEmpty",
args: args{s: ""},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := HtmlToText(tt.args.s)
if got != tt.want {
t.Errorf("HtmlToText() = '%v', want '%v'", got, tt.want)
}
})
}
}

func Test_colorizeQuoteLines(t *testing.T) {
type args struct {
s string
}
tests := []struct {
name string
args args
want string
}{
{
name: "TestQuote",
args: args{s: "> foo\nbar"},
want: "> foo\nbar", // I'm not sure why ansi escape codes are not being rendered here
},
{
name: "TestEmpty",
args: args{s: ""},
want: "",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := colorizeQuoteLines(tt.args.s); got != tt.want {
t.Errorf("colorizeQuoteLines() = '%v', want '%v'", got, tt.want)
}
})
}
}