gromer

#golang#htmx#ssr

git clone https://git.pyrossh.dev/gromer

gromer is a framework and cli to build multipage web apps in golang using htmx and alpinejs.


74b3ac4 — Peter John 2022-07-26T09:52:38+05:30
improve router testing

Generated files not shown: go.sum

_example/components/init.go ADDED
@@ -0,0 +1,8 @@
1
+ package components
2
+
3
+ import "github.com/pyros2097/gromer/gsx"
4
+
5
+ func init() {
6
+ gsx.RegisterComponent(Todo, TodoStyles, "todo")
7
+ gsx.RegisterComponent(Status, StatusStyles, "status", "error")
8
+ }
_example/containers/init.go ADDED
@@ -0,0 +1,8 @@
1
+ package containers
2
+
3
+ import "github.com/pyros2097/gromer/gsx"
4
+
5
+ func init() {
6
+ gsx.RegisterComponent(TodoCount, nil, "filter")
7
+ gsx.RegisterComponent(TodoList, nil, "page", "filter")
8
+ }
_example/main.go CHANGED
@@ -1,46 +1,11 @@
1
1
  package main
2
2
 
3
3
  import (
4
- "github.com/gorilla/mux"
5
4
  "github.com/pyros2097/gromer"
6
- gromer_assets "github.com/pyros2097/gromer/assets"
7
- "github.com/pyros2097/gromer/gsx"
8
- "github.com/rs/zerolog/log"
9
- "gocloud.dev/server"
10
-
11
- "github.com/pyros2097/gromer/_example/assets"
12
- "github.com/pyros2097/gromer/_example/components"
5
+ _ "github.com/pyros2097/gromer/_example/components"
13
- "github.com/pyros2097/gromer/_example/containers"
14
- "github.com/pyros2097/gromer/_example/routes"
6
+ _ "github.com/pyros2097/gromer/_example/routes"
15
7
  )
16
8
 
17
- func init() {
18
- gsx.RegisterComponent(components.Todo, components.TodoStyles, "todo")
19
- gsx.RegisterComponent(components.Status, components.StatusStyles, "status", "error")
20
- gsx.RegisterComponent(containers.TodoCount, nil, "filter")
21
- gsx.RegisterComponent(containers.TodoList, nil, "page", "filter")
22
- }
23
-
24
9
  func main() {
25
- baseRouter := mux.NewRouter()
26
- baseRouter.Use(gromer.LogMiddleware)
27
- gromer.RegisterStatusHandler(baseRouter, components.Status)
28
-
29
- staticRouter := baseRouter.NewRoute().Subrouter()
30
- staticRouter.Use(gromer.CacheMiddleware)
31
- staticRouter.Use(gromer.CompressMiddleware)
32
- gromer.StaticRoute(staticRouter, "/gromer/", gromer_assets.FS)
33
- gromer.StaticRoute(staticRouter, "/assets/", assets.FS)
34
- gromer.IconsRoute(staticRouter, "/icons/", assets.FS)
35
- gromer.ComponentStylesRoute(staticRouter, "/components.css")
36
-
37
- pageRouter := baseRouter.NewRoute().Subrouter()
38
- gromer.PageRoute(pageRouter, "/", routes.TodosPage, routes.TodosAction)
39
- gromer.PageRoute(pageRouter, "/about", routes.AboutPage, nil)
40
-
41
- log.Info().Msg("http server listening on http://localhost:3000")
42
- srv := server.New(baseRouter, nil)
43
- if err := srv.ListenAndServe(":3000"); err != nil {
10
+ gromer.Run("3000")
44
- log.Fatal().Stack().Err(err).Msg("failed to listen")
45
- }
46
11
  }
_example/makefile CHANGED
@@ -1,7 +1,6 @@
1
1
  setup:
2
2
  go install github.com/mitranim/gow@latest
3
- go install github.com/playwright-community/playwright-go/cmd/playwright@latest
3
+ go run github.com/playwright-community/playwright-go/cmd/playwright install --with-deps
4
- playwright install --with-deps
5
4
 
6
5
  dev:
7
6
  gow run main.go
_example/readme.md CHANGED
@@ -11,7 +11,6 @@ This example demonstrates gromer with a simple todo web app.
11
11
 
12
12
  ```sh
13
13
  make setup
14
- make update
15
14
  make dev
16
15
  make build
17
16
  make run
_example/routes/init.go ADDED
@@ -0,0 +1,13 @@
1
+ package routes
2
+
3
+ import (
4
+ "github.com/pyros2097/gromer"
5
+ "github.com/pyros2097/gromer/_example/components"
6
+ "github.com/pyros2097/gromer/assets"
7
+ )
8
+
9
+ func init() {
10
+ gromer.Init(components.Status, assets.FS)
11
+ gromer.PageRoute("/", TodosPage, TodosAction)
12
+ gromer.PageRoute("/about", AboutPage, nil)
13
+ }
_example/tests/todos_test.go CHANGED
@@ -1,47 +1,78 @@
1
1
  package tests
2
2
 
3
3
  import (
4
- "fmt"
4
+ "net/http/httptest"
5
5
  "testing"
6
6
 
7
7
  "github.com/playwright-community/playwright-go"
8
+ "github.com/pyros2097/gromer"
9
+ _ "github.com/pyros2097/gromer/_example/routes"
10
+ "github.com/stretchr/testify/require"
8
11
  )
9
12
 
10
- func TestTodo(t *testing.T) {
13
+ func setup(t *testing.T) (*require.Assertions, playwright.Page, func()) {
14
+ r := require.New(t)
15
+ server := httptest.NewServer(gromer.GetRouter())
11
16
  pw, err := playwright.Run()
12
- if err != nil {
17
+ r.NoError(err)
13
- t.Fatalf("could not start playwright: %v", err)
18
+ browser, err := pw.Chromium.Launch(playwright.BrowserTypeLaunchOptions{
19
+ Headless: playwright.Bool(true),
14
- }
20
+ })
15
- browser, err := pw.Chromium.Launch()
16
- if err != nil {
21
+ r.NoError(err)
17
- t.Fatalf("could not launch browser: %v", err)
18
- }
19
22
  page, err := browser.NewPage()
23
+ r.NoError(err)
24
+ _, err = page.Goto(server.URL)
25
+ r.NoError(err)
26
+ return r, page, func() {
27
+ server.Close()
28
+ err = browser.Close()
29
+ r.NoError(err)
20
- if err != nil {
30
+ err = pw.Stop()
21
- t.Fatalf("could not create page: %v", err)
31
+ r.NoError(err)
22
32
  }
23
- if _, err = page.Goto("https://news.ycombinator.com"); err != nil {
24
- t.Fatalf("could not goto: %v", err)
25
- }
33
+ }
34
+
35
+ func checkAttr(r *require.Assertions, el playwright.ElementHandle, exp string) {
26
- entries, err := page.QuerySelectorAll(".athing")
36
+ src, err := el.GetAttribute("src")
37
+ r.NoError(err)
38
+ r.Equal(exp, src)
39
+ }
40
+
41
+ func getEl(el playwright.ElementHandle, s string) playwright.ElementHandle {
42
+ v, err := el.QuerySelector(s)
27
43
  if err != nil {
28
- t.Fatalf("could not get entries: %v", err)
29
- }
30
- for i, entry := range entries {
31
- titleElement, err := entry.QuerySelector("td.title > a")
32
- if err != nil {
44
+ panic(err)
33
- t.Fatalf("could not get title element: %v", err)
34
- }
35
- title, err := titleElement.TextContent()
36
- if err != nil {
37
- t.Fatalf("could not get text content: %v", err)
38
- }
39
- fmt.Printf("%d: %s\n", i+1, title)
40
- }
41
- if err = browser.Close(); err != nil {
42
- t.Fatalf("could not close browser: %v", err)
43
- }
44
- if err = pw.Stop(); err != nil {
45
- t.Fatalf("could not stop Playwright: %v", err)
46
45
  }
46
+ return v
47
+ }
48
+
49
+ func TestTodoPage(t *testing.T) {
50
+ r, page, close := setup(t)
51
+ defer close()
52
+ el, err := page.QuerySelector(".title")
53
+ r.NoError(err)
54
+ title, err := el.TextContent()
55
+ r.NoError(err)
56
+ r.Contains(title, "todos")
57
+ }
58
+
59
+ func TestAddTodo(t *testing.T) {
60
+ r, page, close := setup(t)
61
+ defer close()
62
+ page.Type("#text", "First Todo")
63
+ page.Keyboard().Down("Enter")
64
+ els, err := page.QuerySelectorAll(".Todo")
65
+ r.NoError(err)
66
+ r.Len(els, 1)
67
+ todo := els[0]
68
+ title, err := todo.TextContent()
69
+ r.NoError(err)
70
+ r.Contains(title, "First Todo")
71
+ img, err := todo.QuerySelector("img")
72
+ r.NoError(err)
73
+ checkAttr(r, img, "/icons/unchecked.svg?fill=gray-400")
74
+ getEl(todo, ".button-1").Click()
75
+ els, err = page.QuerySelectorAll(".Todo")
76
+ r.NoError(err)
77
+ checkAttr(r, getEl(els[0], "img"), "/icons/checked.svg?fill=green-500")
47
78
  }
assets/assets.go CHANGED
@@ -1,4 +1,4 @@
1
- package gromer_assets
1
+ package assets
2
2
 
3
3
  import (
4
4
  "embed"
go.mod CHANGED
@@ -4,10 +4,8 @@ go 1.18
4
4
 
5
5
  require (
6
6
  github.com/alecthomas/participle/v2 v2.0.0-beta.3
7
- github.com/alecthomas/repr v0.1.0
8
7
  github.com/felixge/httpsnoop v1.0.1
9
8
  github.com/go-playground/validator/v10 v10.9.0
10
- github.com/gobuffalo/velvet v0.0.0-20170320144106-d97471bf5d8f
11
9
  github.com/goneric/stack v0.0.0-20220131052059-5990ae324dbd
12
10
  github.com/google/uuid v1.3.0
13
11
  github.com/gorilla/handlers v1.5.1
@@ -19,49 +17,40 @@ require (
19
17
  github.com/segmentio/go-camelcase v0.0.0-20160726192923-7085f1e3c734
20
18
  github.com/stretchr/testify v1.7.0
21
19
  gocloud.dev v0.24.0
22
- golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57
23
20
  xojoc.pw/useragent v0.0.0-20200116211053-1ec61d55e8fe
24
21
  )
25
22
 
26
23
  require (
27
24
  cloud.google.com/go v0.94.0 // indirect
28
25
  cloud.google.com/go/firestore v1.5.0 // indirect
29
- github.com/aymerick/douceur v0.2.0 // indirect
30
- github.com/aymerick/raymond v2.0.2+incompatible // indirect
31
26
  github.com/blang/semver v3.5.1+incompatible // indirect
27
+ github.com/chromedp/cdproto v0.0.0-20220515234810-83d799542a04 // indirect
28
+ github.com/chromedp/chromedp v0.8.2 // indirect
29
+ github.com/chromedp/sysutil v1.0.0 // indirect
30
+ github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 // indirect
32
31
  github.com/davecgh/go-spew v1.1.1 // indirect
33
32
  github.com/go-playground/locales v0.14.0 // indirect
34
33
  github.com/go-playground/universal-translator v0.18.0 // indirect
34
+ github.com/go-stack/stack v1.8.1 // indirect
35
+ github.com/gobwas/httphead v0.1.0 // indirect
36
+ github.com/gobwas/pool v0.2.1 // indirect
35
- github.com/gobuffalo/envy v1.6.5 // indirect
37
+ github.com/gobwas/ws v1.1.0 // indirect
36
38
  github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
37
39
  github.com/golang/protobuf v1.5.2 // indirect
38
40
  github.com/google/go-cmp v0.5.6 // indirect
39
41
  github.com/google/wire v0.5.0 // indirect
40
42
  github.com/googleapis/gax-go/v2 v2.1.0 // indirect
41
- github.com/gorilla/css v1.0.0 // indirect
42
- github.com/joho/godotenv v1.3.0 // indirect
43
+ github.com/josharian/intern v1.0.0 // indirect
43
44
  github.com/leodido/go-urn v1.2.1 // indirect
44
- github.com/markbates/inflect v1.0.4 // indirect
45
- github.com/microcosm-cc/bluemonday v1.0.18 // indirect
46
- github.com/pkg/errors v0.9.1 // indirect
45
+ github.com/mailru/easyjson v0.7.7 // indirect
46
+ github.com/playwright-community/playwright-go v0.2000.1 // indirect
47
47
  github.com/pmezard/go-difflib v1.0.0 // indirect
48
- github.com/russross/blackfriday v1.5.2 // indirect
49
- github.com/sergi/go-diff v1.2.0 // indirect
50
- github.com/shurcooL/github_flavored_markdown v0.0.0-20210228213109-c3a9aa474629 // indirect
51
- github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636 // indirect
52
- github.com/shurcooL/go-goon v1.0.0 // indirect
53
- github.com/shurcooL/highlight_diff v0.0.0-20181222201841-111da2e7d480 // indirect
54
- github.com/shurcooL/highlight_go v0.0.0-20191220051317-782971ddf21b // indirect
55
- github.com/shurcooL/octicon v0.0.0-20191102190552-cbb32d6a785c // indirect
56
- github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
57
- github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d // indirect
58
- github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e // indirect
59
48
  go.opencensus.io v0.23.0 // indirect
60
49
  golang.org/x/crypto v0.0.0-20211215165025-cf75a172585e // indirect
61
50
  golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
62
51
  golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
63
52
  golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
64
- golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect
53
+ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect
65
54
  golang.org/x/text v0.3.7 // indirect
66
55
  golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
67
56
  google.golang.org/api v0.56.0 // indirect
@@ -69,5 +58,6 @@ require (
69
58
  google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2 // indirect
70
59
  google.golang.org/grpc v1.40.0 // indirect
71
60
  google.golang.org/protobuf v1.27.1 // indirect
61
+ gopkg.in/square/go-jose.v2 v2.6.0 // indirect
72
62
  gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
73
63
  )
http.go CHANGED
@@ -23,11 +23,13 @@ import (
23
23
  "github.com/google/uuid"
24
24
  "github.com/gorilla/handlers"
25
25
  "github.com/gorilla/mux"
26
+ "github.com/pyros2097/gromer/assets"
26
27
  "github.com/pyros2097/gromer/gsx"
27
28
  "github.com/rotisserie/eris"
28
29
  "github.com/rs/zerolog"
29
30
  "github.com/rs/zerolog/log"
30
31
  "github.com/segmentio/go-camelcase"
32
+ "gocloud.dev/server"
31
33
  "xojoc.pw/useragent"
32
34
  )
33
35
 
@@ -36,6 +38,8 @@ var (
36
38
  IsCloundRun bool
37
39
  pathParamsRegex = regexp.MustCompile(`{(.*?)}`)
38
40
  globalStatusComponent StatusComponent = nil
41
+ baseRouter = &mux.Router{}
42
+ pageRouter = &mux.Router{}
39
43
  )
40
44
 
41
45
  type StatusComponent func(c *gsx.Context, status int, err error) []*gsx.Tag
@@ -334,8 +338,8 @@ func RegisterStatusHandler(router *mux.Router, comp StatusComponent) {
334
338
  })
335
339
  }
336
340
 
337
- func PageRoute(router *mux.Router, route string, page, action interface{}) {
341
+ func PageRoute(route string, page, action interface{}) {
338
- router.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
342
+ pageRouter.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
339
343
  c := createCtx(r, route)
340
344
  if r.Method == "GET" {
341
345
  PerformRequest(route, page, c, w, r, false)
@@ -388,3 +392,30 @@ func GetComponentsStylesUrl() string {
388
392
  })
389
393
  return fmt.Sprintf("/components.css?hash=%s", sum)
390
394
  }
395
+
396
+ func Init(status StatusComponent, appAssets embed.FS) {
397
+ baseRouter = mux.NewRouter()
398
+ baseRouter.Use(LogMiddleware)
399
+ RegisterStatusHandler(baseRouter, status)
400
+
401
+ staticRouter := baseRouter.NewRoute().Subrouter()
402
+ staticRouter.Use(CacheMiddleware)
403
+ staticRouter.Use(CompressMiddleware)
404
+ StaticRoute(staticRouter, "/gromer/", assets.FS)
405
+ StaticRoute(staticRouter, "/assets/", appAssets)
406
+ IconsRoute(staticRouter, "/icons/", appAssets)
407
+ ComponentStylesRoute(staticRouter, "/components.css")
408
+ pageRouter = baseRouter.NewRoute().Subrouter()
409
+ }
410
+
411
+ func GetRouter() *mux.Router {
412
+ return baseRouter
413
+ }
414
+
415
+ func Run(port string) {
416
+ log.Info().Msg("http server listening on http://localhost:" + port)
417
+ srv := server.New(baseRouter, nil)
418
+ if err := srv.ListenAndServe(":" + port); err != nil {
419
+ log.Fatal().Stack().Err(err).Msg("failed to listen")
420
+ }
421
+ }