Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added unit cypress unit test #4

Merged
merged 2 commits into from
Jan 7, 2024
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
28 changes: 25 additions & 3 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Go
name: Go Build and Cypress Tests

on:
push:
Expand Down Expand Up @@ -29,5 +29,27 @@ jobs:
go install github.com/a-h/templ/cmd/templ@latest
templ generate

- name: Build
run: go build .
- name: Build & Run
run: |
go build .
go run . &

- uses: actions/setup-node@v3
with:
node-version: 18

- name: Clone cypress-example-todomvc
run: git clone https://github.com/cypress-io/cypress-example-todomvc.git cypress-example-todomvc

- name: Install Dependencies
run: |
cd cypress-example-todomvc
npm install

- name: Run Cypress Tests
run: |
cd cypress-example-todomvc
npm run cypress:run

- name: Stop Go Application
run: pkill -f "go run ."
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
todomvc_templ.go
tpl/*.go
go-templ-htmx-_hyperscript.exe
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@
===========================================================
Build with GO, TEMPL, HTMX & _HYPERSCRIPT
[![Go](https://github.com/syarul/todomvc-go-templ-htmx-_hyperscript/actions/workflows/go.yml/badge.svg)](https://github.com/syarul/todomvc-go-templ-htmx-_hyperscript/actions/workflows/go.yml)

### Testing
As evidence of HTMX's capabilities in emulating the functionalities of modern frameworks, I have incorporated [unit test](https://github.com/syarul/todomvc-go-templ-htmx-_hyperscript/actions/runs/7412273948/job/20168687544) from https://github.com/cypress-io/cypress-example-todomvc. This demonstration serves to showcase that HTMX, when paired with _hyperscript, can replicate all the behaviors typically associated with most modern client frameworks.

### Security
Check on [this link](https://templ.guide/security/) when using `templ` as HTML template engine. At anytime as developer `Do not blame the farmer if you cook the rice til it burns.`

### Usage
- install go if you don't have
- run `go mod tidy` to fetch all needed modules
Expand All @@ -19,4 +26,8 @@
- alternatively you can compile into executable with `go build .`

### HTMX
Visit [https://github.com/rajasegar/awesome-htmx](https://github.com/rajasegar/awesome-htmx) to look for HTMX curated infos
Visit [https://github.com/rajasegar/awesome-htmx](https://github.com/rajasegar/awesome-htmx) to look for HTMX curated infos

###
Todo
- Use behavior to modular the _hyperscript scripts
5 changes: 1 addition & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,4 @@ module todomvc/go-templ-htmx-_hyperscript

go 1.21.5

require (
github.com/a-h/templ v0.2.501
github.com/google/uuid v1.5.0
)
require github.com/a-h/templ v0.2.501
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,3 @@ github.com/a-h/templ v0.2.501 h1:9rIo5u+B+NDJIkbHGthckUGRguCuWKY/7ri8e2ckn9M=
github.com/a-h/templ v0.2.501/go.mod h1:9gZxTLtRzM3gQxO8jr09Na0v8/jfliS97S9W5SScanM=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
150 changes: 99 additions & 51 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
package main

import (
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
"todomvc/go-templ-htmx-_hyperscript/tpl"

"github.com/a-h/templ"
)

var idCounter uint64

type Todo struct {
id uint64
Id uint64 `json:"id"`
title string
done bool
Done bool `json:"done"`
editing bool
}

Expand Down Expand Up @@ -50,7 +54,7 @@ func (t *todos) crudOps(action Action, todo Todo) Todo {
index := -1
if action != Create {
for i, r := range *t {
if r.id == todo.id {
if r.Id == todo.Id {
index = i
break
}
Expand All @@ -61,7 +65,7 @@ func (t *todos) crudOps(action Action, todo Todo) Todo {
*t = append(*t, todo)
return todo
case Toggle:
(*t)[index].done = todo.done
(*t)[index].Done = todo.Done
case Update:
title := strings.Trim(todo.title, " ")
if len(title) != 0 {
Expand All @@ -88,12 +92,14 @@ func main() {
t := &todos{}

// Register the routes.
http.Handle("/get-hash", http.HandlerFunc(t.getHash))
// http.Handle("/get-hash", http.HandlerFunc(t.getHash))
http.Handle("/set-hash", http.HandlerFunc(setHash))
http.Handle("/learn.json", http.HandlerFunc(learnHandler))

http.Handle("/update-counts", http.HandlerFunc(t.updateCounts))
http.Handle("/toggle-all", http.HandlerFunc(t.toggleAllHandler))
http.Handle("/completed", http.HandlerFunc(t.clearCompleted))
http.Handle("/footer", http.HandlerFunc(t.footerHandler))

http.Handle("/", http.HandlerFunc(t.pageHandler))

Expand All @@ -105,15 +111,18 @@ func main() {

http.Handle("/toggle-main", http.HandlerFunc(t.toggleMainHandler))
http.Handle("/toggle-footer", http.HandlerFunc(t.toggleFooterHandler))
http.Handle("/todo-list", http.HandlerFunc(t.todoListHandler))
http.Handle("/todo-json", http.HandlerFunc(t.getJSON))
http.Handle("/todo-item", http.HandlerFunc(t.todoItemHandler))

// Specify the directory containing your static files
dir := "./assets"
// this is used to serve axe-core for the todomvc test
dir := "./cypress-example-todomvc/node_modules"

// Use the http.FileServer to create a handler for serving static files
fs := http.FileServer(http.Dir(dir))

// Use the http.Handle to register the file server handler for a specific route
http.Handle("/assets/", http.StripPrefix("/assets/", fs))
http.Handle("/node_modules/", http.StripPrefix("/node_modules/", fs))

// start the server.
addr := os.Getenv("LISTEN_ADDRESS")
Expand All @@ -133,7 +142,7 @@ func main() {
func countNotDone(todos []Todo) int {
count := 0
for _, todo := range todos {
if !todo.done {
if !todo.Done {
count++
}
}
Expand All @@ -155,7 +164,7 @@ func defChecked(todos []Todo) bool {
// has completeTask checks if there is any completed task in the Todos slice
func hasCompleteTask(todos []Todo) bool {
for _, todo := range todos {
if todo.done {
if todo.Done {
return true
}
}
Expand Down Expand Up @@ -183,29 +192,30 @@ func learnHandler(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(emptyJSON)
}

// getHash handles the GET request for the #/:name route.
// it updates the selected field of each filter based on the name query parameter.
// on initial fetch when todos is empty it will send "empty string" which usually
// ignored by htmx, the reason here we want to make use templ rendering to behave
// efficiently by rendering only needed html, this use case usually available in
// modern client framework i.e., in React you can do
// <>{todo.length && <TodoList />}</>
// so you can do the same in HTMX too!
func (t *todos) getHash(w http.ResponseWriter, r *http.Request) {
if len(*t) == 0 {
byteRenderer(w, r, "")
return
// this for acquiring todos as json where client can fetch todo to render when route change
// its pretty much as the same how react do DOM diffing instead we do it on server and send
// the needed rendered HTML as client check which one is missing
func (t *todos) getJSON(w http.ResponseWriter, r *http.Request) {
// set the Content-Type header to indicate JSON
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(*t)
}

func selectedFilter(filters []Filter) string {
for _, filter := range filters {
if filter.selected {
return filter.name
}
}
return "All"
}

func setHash(w http.ResponseWriter, r *http.Request) {

hash := r.FormValue("hash")
name := r.FormValue("name")

if len(name) == 0 {
if len(hash) != 0 {
name = hash
} else {
name = "All"
}
name = "All"
}
// loop through filters and update the selected field
for i := range filters {
Expand All @@ -216,15 +226,51 @@ func (t *todos) getHash(w http.ResponseWriter, r *http.Request) {
}
}

// render the filter component with the updated filters
templRenderer(w, r, filter(filters))
byteRenderer(w, r, "")
}

func generateRandomString(length int) (string, error) {
bytes := make([]byte, length)
_, err := rand.Read(bytes)
if err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(bytes), nil
}

func (t *todos) footerHandler(w http.ResponseWriter, r *http.Request) {
templRenderer(w, r, footer(*t, filters, hasCompleteTask(*t)))
}

func (t *todos) pageHandler(w http.ResponseWriter, r *http.Request) {
// start with new todo data when refresh
// *t = make([]Todo, 0)
// idCounter = 0
templRenderer(w, r, Page(*t, filters, defChecked(*t), hasCompleteTask(*t)))
_, err := r.Cookie("sessionId")

if err == http.ErrNoCookie {
// fmt.Println("Error:", err)
newCookieValue, err := generateRandomString(32)
if err != nil {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}

newCookie := http.Cookie{
Name: "sessionId",
Value: newCookieValue,
Expires: time.Now().Add(time.Second * 6000),
HttpOnly: true,
}
http.SetCookie(w, &newCookie)

// start with new todo data when session is reset
*t = make([]Todo, 0)
idCounter = 0
}

templRenderer(w, r, Page(*t, filters, defChecked(*t), hasCompleteTask(*t), selectedFilter(filters)))
}

func (t *todos) todoListHandler(w http.ResponseWriter, r *http.Request) {
templRenderer(w, r, todoList(*t, selectedFilter(filters)))
}

// toggle section main
Expand All @@ -251,18 +297,31 @@ func (t *todos) addTodoHandler(w http.ResponseWriter, r *http.Request) {
todo := t.crudOps(Create, Todo{id, title, false, false})

if len(*t) == 1 {
templRenderer(w, r, todoList(*t))
templRenderer(w, r, todoList(*t, selectedFilter(filters)))
} else {
templRenderer(w, r, todoItem(todo))
templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
}

}

func (t *todos) todoItemHandler(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseUint(r.FormValue("id"), 0, 32)

if err != nil {
fmt.Println("Error:", err)
return
}

todo := t.crudOps(Edit, Todo{id, "", false, false})

templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
}

func (t *todos) clearCompleted(w http.ResponseWriter, r *http.Request) {
// determine render "none" or "block" based on incomplete tasks
hasCompleted := hasCompleteTask(*t)
if hasCompleted {
templRenderer(w, r, clearCompleted(hasCompleteTask(*t)))
templRenderer(w, r, tpl.ClearCompleted(hasCompleteTask(*t)))
} else {
byteRenderer(w, r, "")
}
Expand Down Expand Up @@ -302,7 +361,7 @@ func (t *todos) toggleTodo(w http.ResponseWriter, r *http.Request) {

todo := t.crudOps(Toggle, Todo{id, "", !done, false})

templRenderer(w, r, todoItem(todo))
templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
}

func (t *todos) editTodoHandler(w http.ResponseWriter, r *http.Request) {
Expand All @@ -320,7 +379,7 @@ func (t *todos) editTodoHandler(w http.ResponseWriter, r *http.Request) {
// since editing only client side changes
todo := t.crudOps(Edit, Todo{id, "", false, false})

templRenderer(w, r, editTodo(Todo{id, todo.title, todo.done, true}))
templRenderer(w, r, editTodo(Todo{id, todo.title, todo.Done, true}))
}

func (t *todos) updateTodo(w http.ResponseWriter, r *http.Request) {
Expand All @@ -331,25 +390,14 @@ func (t *todos) updateTodo(w http.ResponseWriter, r *http.Request) {
return
}

// esc, err := strconv.ParseBool(r.FormValue("esc"))
// if err != nil {
// fmt.Println("Error:", err)
// return
// }

title := r.FormValue("title")

// esc will not do update instead will send the original unmodified title
// if esc {
// todo := t.crudOps(Edit, Todo{id, "", false, false})
// templRenderer(w, r, todoItem(todo))
// } else {
todo := t.crudOps(Update, Todo{id, title, false, false})
if len(todo.title) == 0 {
byteRenderer(w, r, "")
return
}
templRenderer(w, r, todoItem(todo))
templRenderer(w, r, todoItem(todo, selectedFilter(filters)))
// }
}

Expand Down
Loading
Loading