~repos /gromer

#golang#htmx#ssr

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

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


b6e0e394 pyros2097

tag: v0.2.7

v0.2.7

4 years ago
expose NewElement
Files changed (6) hide show
  1. attributes.go +2 -1
  2. element.go +4 -0
  3. html.go +17 -23
  4. raw_test.go +158 -114
  5. testing.go +141 -135
  6. testing_test.go +10 -10
attributes.go CHANGED
@@ -123,7 +123,7 @@ type HelmetDescription string
123
123
  type HelmetAuthor string
124
124
  type HelmetKeywords string
125
125
 
126
- func mergeAttributes(parent *Element, uis ...interface{}) {
126
+ func MergeAttributes(parent *Element, uis ...interface{}) *Element {
127
127
  elems := []UI{}
128
128
  for _, v := range uis {
129
129
  switch c := v.(type) {
@@ -160,4 +160,5 @@ func mergeAttributes(parent *Element, uis ...interface{}) {
160
160
  }
161
161
  }
162
162
  parent.setBody(elems)
163
+ return parent
163
164
  }
element.go CHANGED
@@ -18,6 +18,10 @@ type Element struct {
18
18
  this UI
19
19
  }
20
20
 
21
+ func NewElement(tag string, selfClosing bool, uis ...interface{}) *Element {
22
+ return MergeAttributes(&Element{tag: tag, selfClosing: selfClosing}, uis...)
23
+ }
24
+
21
25
  func (e *Element) JSValue() js.Value {
22
26
  return e.jsvalue
23
27
  }
html.go CHANGED
@@ -51,45 +51,39 @@ func Script(str string) *Element {
51
51
  }
52
52
 
53
53
  func Div(uis ...interface{}) *Element {
54
- e := &Element{tag: "div"}
54
+ return NewElement("div", false, uis)
55
- mergeAttributes(e, uis...)
56
- return e
57
55
  }
58
56
 
59
57
  func A(uis ...interface{}) *Element {
60
- e := &Element{tag: "a"}
58
+ return NewElement("a", false, uis)
59
+ }
60
+
61
+ func P(uis ...interface{}) *Element {
61
- mergeAttributes(e, uis...)
62
+ return NewElement("p", false, uis)
63
+ }
64
+
65
+ func Span(uis ...interface{}) *Element {
62
- return e
66
+ return NewElement("span", false, uis)
63
67
  }
64
68
 
65
69
  func Input(uis ...interface{}) *Element {
66
- e := &Element{tag: "input"}
70
+ return NewElement("input", false, uis)
67
- mergeAttributes(e, uis...)
68
- return e
69
71
  }
70
72
 
71
73
  func Image(uis ...interface{}) *Element {
72
- e := &Element{tag: "image"}
74
+ return NewElement("image", false, uis)
73
- mergeAttributes(e, uis...)
74
- return e
75
75
  }
76
76
 
77
77
  func Button(uis ...interface{}) *Element {
78
- e := &Element{tag: "button"}
78
+ return NewElement("button", false, uis)
79
- mergeAttributes(e, uis...)
80
- return e
81
79
  }
82
80
 
83
- func Svg(elems ...interface{}) *Element {
81
+ func Svg(uis ...interface{}) *Element {
84
- e := &Element{tag: "svg"}
82
+ return NewElement("svg", false, uis)
85
- mergeAttributes(e, elems...)
86
- return e
87
83
  }
88
84
 
89
- func SvgText(elems ...interface{}) *Element {
85
+ func SvgText(uis ...interface{}) *Element {
90
- e := &Element{tag: "text"}
86
+ return NewElement("text", false, uis)
91
- mergeAttributes(e, elems...)
92
- return e
93
87
  }
94
88
 
95
89
  func Row(uis ...interface{}) UI {
raw_test.go CHANGED
@@ -1,116 +1,160 @@
1
1
  package app
2
2
 
3
- // import (
4
- // "testing"
5
-
6
- // "github.com/stretchr/testify/require"
7
- // )
8
-
9
- // func TestRawRootTagName(t *testing.T) {
10
- // tests := []struct {
11
- // scenario string
12
- // raw string
13
- // expected string
14
- // }{
15
- // {
16
- // scenario: "tag set",
17
- // raw: `
18
- // <div>
19
- // <span></span>
20
- // </div>`,
21
- // expected: "div",
22
- // },
23
- // {
24
- // scenario: "tag is empty",
25
- // },
26
- // {
27
- // scenario: "opening tag missing",
28
- // raw: "</div>",
29
- // },
30
- // {
31
- // scenario: "tag is not set",
32
- // raw: "div",
33
- // },
34
- // {
35
- // scenario: "tag is not closing",
36
- // raw: "<div",
37
- // },
38
- // {
39
- // scenario: "tag is not closing",
40
- // raw: "<div",
41
- // },
42
- // {
43
- // scenario: "tag without value",
44
- // raw: "<>",
45
- // },
46
- // }
47
-
48
- // for _, test := range tests {
49
- // t.Run(test.scenario, func(t *testing.T) {
50
- // tag := rawRootTagName(test.raw)
51
- // require.Equal(t, test.expected, tag)
52
- // })
53
- // }
54
- // }
55
-
56
- // func TestRawMountDismount(t *testing.T) {
57
- // testMountDismount(t, []mountTest{
58
- // {
59
- // scenario: "raw html element",
60
- // node: Raw(`<h1>Hello</h1>`),
61
- // },
62
- // {
63
- // scenario: "raw svg element",
64
- // node: Raw(`<svg></svg>`),
65
- // },
66
- // })
67
- // }
68
-
69
- // func TestRawUpdate(t *testing.T) {
70
- // testUpdate(t, []updateTest{
71
- // {
72
- // scenario: "raw html element returns replace error when updated with a non text-element",
73
- // a: Raw("<svg></svg>"),
74
- // b: Div(),
75
- // replaceErr: true,
76
- // },
77
- // {
78
- // scenario: "raw html element is replace by another raw html element",
79
- // a: Div().Body(
80
- // Raw("<div></div>"),
81
- // ),
82
- // b: Div().Body(
83
- // Raw("<svg></svg>"),
84
- // ),
85
- // matches: []TestUIDescriptor{
86
- // {
87
- // Path: TestPath(),
88
- // Expected: Div(),
89
- // },
90
- // {
91
- // Path: TestPath(0),
92
- // Expected: Raw("<svg></svg>"),
93
- // },
94
- // },
95
- // },
96
- // {
97
- // scenario: "raw html element is replace by non-raw html element",
98
- // a: Div().Body(
99
- // Raw("<div></div>"),
100
- // ),
101
- // b: Div().Body(
102
- // Text("hello"),
103
- // ),
104
- // matches: []TestUIDescriptor{
105
- // {
106
- // Path: TestPath(),
107
- // Expected: Div(),
108
- // },
109
- // {
110
- // Path: TestPath(0),
111
- // Expected: Text("hello"),
112
- // },
113
- // },
114
- // },
115
- // })
116
- // }
3
+ import (
4
+ "testing"
5
+
6
+ "github.com/stretchr/testify/require"
7
+ )
8
+
9
+ var rawhtml = `
10
+ <div>
11
+ <p>slug: &ldquo;/blog/gopibot-to-the-rescue&rdquo;
12
+ date: &ldquo;2017-10-04&rdquo;</p>
13
+
14
+ <h2>title: &ldquo;Gopibot To The Rescue&rdquo;</h2>
15
+
16
+ <p>High Ho Gopibot away!</p>
17
+
18
+ <p>Everybody please meet Gopibot our chatops bot which I built at Numberz to help us deploy our countless microservices to QA.</p>
19
+
20
+ <p><img src="../images/posts/gopibot1.png" alt="Gopibot 1" /></p>
21
+
22
+ <p>So here is the backstory,</p>
23
+
24
+ <p>I was one of the developers who had access to our QA and Prod servers and the other person was the Head of Engineering
25
+ and he is generally a busy guy. So whenever there is a change that needs to be deployed everyone comes to me and tells me
26
+ to deploy their microservice/frontend to the QA and blatantly interrupts my awesome coding cycle.</p>
27
+
28
+ <p>Alright then, I break off from my flow, ssh into the server and start running the deploy command.
29
+ And all of you jsdev wannabes who have worked with react and webpack will know the horrors about deploying frontend code right.
30
+ It takes forever so I have to wait there looking at the console along with the dev who wanted me to deploy it (lets call him kokill for now).
31
+ So kokill and I patiently wait for the webpack build to finish. 1m , 2m, 3m and WTH 15m.
32
+ And then its built and the new frontend is deployed to QA. YES! Now I can continue with my work.
33
+ But wait then some other dev comes likes call him (D-Ne0) and he asks to deploy something else and again the same process
34
+ of ssh’ing the server and another wait. This got repetitive and irritating. Then I started searching for solutions to the problem
35
+ and looked high and low and thought that CI/CD is the only thing that can solve this problem. But then I saw something new called ChatOps
36
+ where developers have chatbots to talk to automate this manual work. Just like we have bots these days to help you out in your work
37
+ like getting your laundry, grocery and making orders.</p>
38
+
39
+ <p>So I decided to take a shot at this in my free time. And it seems it was simpler than I thought and decided to use Slack our primary
40
+ team communication platform. We used it daily for everything and I thought why not have a specific channel just where the bot resides
41
+ and people could talk to the bot.</p>
42
+
43
+ <p>Since we are typically a nodejs shop I decided to find a way to send messages to a slack bot. And slack has this really great sdk for nodejs.
44
+ <a href="https://github.com/slackapi/node-slack-sdk">https://github.com/slackapi/node-slack-sdk</a></p>
45
+
46
+ <p>First I went and created the bot in my slack team settings.
47
+ And then wrote a script which would allow it to read messages from the channel it was added.</p>
48
+
49
+ <p>Here is the simple script,</p>
50
+ </div>
51
+ `
52
+
53
+ func TestRawRootTagName(t *testing.T) {
54
+ tests := []struct {
55
+ scenario string
56
+ raw string
57
+ expected string
58
+ }{
59
+ {
60
+ scenario: "tag set",
61
+ raw: `
62
+ <div>
63
+ <span></span>
64
+ </div>`,
65
+ expected: "div",
66
+ },
67
+ {
68
+ scenario: "tag is empty",
69
+ },
70
+ {
71
+ scenario: "opening tag missing",
72
+ raw: "</div>",
73
+ },
74
+ {
75
+ scenario: "tag is not set",
76
+ raw: "div",
77
+ },
78
+ {
79
+ scenario: "tag is not closing",
80
+ raw: "<div",
81
+ },
82
+ {
83
+ scenario: "tag is not closing",
84
+ raw: "<div",
85
+ },
86
+ {
87
+ scenario: "tag without value",
88
+ raw: "<>",
89
+ },
90
+ }
91
+
92
+ for _, test := range tests {
93
+ t.Run(test.scenario, func(t *testing.T) {
94
+ tag := rawRootTagName(test.raw)
95
+ require.Equal(t, test.expected, tag)
96
+ })
97
+ }
98
+ }
99
+
100
+ func TestRawMountDismount(t *testing.T) {
101
+ testMountDismount(t, []mountTest{
102
+ {
103
+ scenario: "raw html element",
104
+ node: Raw(`<h1>Hello</h1>`),
105
+ },
106
+ {
107
+ scenario: "raw svg element",
108
+ node: Raw(`<svg></svg>`),
109
+ },
110
+ })
111
+ }
112
+
113
+ func TestRawUpdate(t *testing.T) {
114
+ testUpdate(t, []updateTest{
115
+ {
116
+ scenario: "raw html element returns replace error when updated with a non text-element",
117
+ a: Raw("<svg></svg>"),
118
+ b: Div(),
119
+ replaceErr: true,
120
+ },
121
+ {
122
+ scenario: "raw html element is replace by another raw html element",
123
+ a: Div(
124
+ Raw("<div></div>"),
125
+ ),
126
+ b: Div(
127
+ Raw("<svg></svg>"),
128
+ ),
129
+ matches: []TestUIDescriptor{
130
+ {
131
+ Path: TestPath(),
132
+ Expected: Div(),
133
+ },
134
+ {
135
+ Path: TestPath(0),
136
+ Expected: Raw("<svg></svg>"),
137
+ },
138
+ },
139
+ },
140
+ {
141
+ scenario: "raw html element is replace by non-raw html element",
142
+ a: Div(
143
+ Raw("<div></div>"),
144
+ ),
145
+ b: Div(
146
+ Text("hello"),
147
+ ),
148
+ matches: []TestUIDescriptor{
149
+ {
150
+ Path: TestPath(),
151
+ Expected: Div(),
152
+ },
153
+ {
154
+ Path: TestPath(0),
155
+ Expected: Text("hello"),
156
+ },
157
+ },
158
+ },
159
+ })
160
+ }
testing.go CHANGED
@@ -1,146 +1,152 @@
1
1
  package app
2
2
 
3
+ import (
4
+ "fmt"
5
+
6
+ "github.com/pyros2097/wapp/errors"
7
+ )
8
+
3
9
  // import (
4
10
  // "fmt"
5
11
 
6
12
  // "github.com/pyros2097/wapp/errors"
7
13
  // )
8
14
 
9
- // // TestUIDescriptor represents a descriptor that describes a UI element and its
10
- // // location from its parents.
11
- // type TestUIDescriptor struct {
12
- // // The location of the node. It is used by the TestMatch to find the
13
- // // element to test.
14
- // //
15
- // // If empty, the expected UI element is compared with the root of the tree.
16
- // //
17
- // // Otherwise, each integer represents the index of the element to traverse,
18
- // // from the root's children to the element to compare
19
- // Path []int
20
-
21
- // // The element to compare with the element targeted by Path. Compare
22
- // // behavior varies depending on the element kind.
23
- // //
24
- // // Simple text elements only have their text value compared.
25
- // //
26
- // // HTML elements have their attribute compared and check if their event
27
- // // handlers are set.
28
- // //
29
- // // Components have their exported field values compared.
30
- // Expected UI
31
- // }
32
-
33
- // // TestPath is a helper function that returns a path to use in a
34
- // // TestUIDescriptor.
35
- // func TestPath(p ...int) []int {
36
- // return p
37
- // }
38
-
39
- // // TestMatch looks for the element targeted by the descriptor in the given tree
40
- // // and reports whether it matches with the expected element.
41
- // //
42
- // // Eg:
43
- // // tree := app.Div().Body(
44
- // // app.H2().Body(
45
- // // app.Text("foo"),
46
- // // ),
47
- // // app.P().Body(
48
- // // app.Text("bar"),
49
- // // ),
50
- // // )
51
- // //
52
- // // // Testing root:
53
- // // err := app.TestMatch(tree, app.TestUIDescriptor{
54
- // // Path: TestPath(),
55
- // // Expected: app.Div(),
56
- // // })
57
- // // // OK => err == nil
58
- // //
59
- // // // Testing h2:
60
- // // err := app.TestMatch(tree, app.TestUIDescriptor{
61
- // // Path: TestPath(0),
62
- // // Expected: app.H3(),
63
- // // })
64
- // // // KO => err != nil because we ask h2 to match with h3
65
- // //
66
- // // // Testing text from p:
67
- // // err = app.TestMatch(tree, app.TestUIDescriptor{
68
- // // Path: TestPath(1, 0),
69
- // // Expected: app.Text("bar"),
70
- // // })
71
- // // // OK => err == nil
72
- // func TestMatch(tree UI, d TestUIDescriptor) error {
73
- // if !tree.Mounted() {
74
- // if err := mount(tree); err != nil {
75
- // return err
76
- // }
77
- // }
78
-
79
- // if d.Expected != nil {
80
- // d.Expected.setSelf(d.Expected)
81
- // }
82
-
83
- // if len(d.Path) != 0 {
84
- // idx := d.Path[0]
85
-
86
- // if idx < 0 || idx >= len(tree.children()) {
87
- // // Check that the element does not exists.
88
- // if d.Expected == nil {
89
- // return nil
90
- // }
91
-
92
- // return errors.New("ui element to match is out of range").
93
- // Tag("name", d.Expected.name()).
94
- // Tag("parent-name", tree.name()).
95
- // Tag("parent-children-count", len(tree.children())).
96
- // Tag("index", idx)
97
- // }
98
-
99
- // c := tree.children()[idx]
100
- // p := c.parent()
101
-
102
- // if p != tree {
103
- // return errors.New("unexpected ui element parent").
104
- // Tag("name", d.Expected.name()).
105
- // Tag("parent-name", p.name()).
106
- // Tag("parent-addr", fmt.Sprintf("%p", p)).
107
- // Tag("expected-parent-name", tree.name()).
108
- // Tag("expected-parent-addr", fmt.Sprintf("%p", tree))
109
- // }
110
-
111
- // d.Path = d.Path[1:]
112
- // return TestMatch(c, d)
113
- // }
114
-
115
- // if d.Expected.name() != tree.name() {
116
- // return errors.New("the UI element is not matching the descriptor").
117
- // Tag("expected-name", d.Expected.name()).
118
- // Tag("current-name", tree.name())
119
- // }
120
-
121
- // // switch d.Expected.Kind() {
122
- // // case SimpleText:
123
- // // return matchText(tree, d)
124
-
125
- // // case HTML:
126
- // // if err := matchHTMLElemAttrs(tree, d); err != nil {
127
- // // return err
128
- // // }
129
- // // return matchHTMLElemEventHandlers(tree, d)
130
-
131
- // // // case Component:
132
- // // // return matchComponent(tree, d)
133
-
134
- // // case RawHTML:
135
- // // return matchRaw(tree, d)
136
-
137
- // // default:
138
- // // return errors.New("the UI element is not matching the descriptor").
139
- // // Tag("reason", "unavailable matching for the kind").
140
- // // Tag("kind", d.Expected.Kind())
141
- // // }
142
- // return nil
143
- // }
15
+ // TestUIDescriptor represents a descriptor that describes a UI element and its
16
+ // location from its parents.
17
+ type TestUIDescriptor struct {
18
+ // The location of the node. It is used by the TestMatch to find the
19
+ // element to test.
20
+ //
21
+ // If empty, the expected UI element is compared with the root of the tree.
22
+ //
23
+ // Otherwise, each integer represents the index of the element to traverse,
24
+ // from the root's children to the element to compare
25
+ Path []int
26
+
27
+ // The element to compare with the element targeted by Path. Compare
28
+ // behavior varies depending on the element kind.
29
+ //
30
+ // Simple text elements only have their text value compared.
31
+ //
32
+ // HTML elements have their attribute compared and check if their event
33
+ // handlers are set.
34
+ //
35
+ // Components have their exported field values compared.
36
+ Expected UI
37
+ }
38
+
39
+ // TestPath is a helper function that returns a path to use in a
40
+ // TestUIDescriptor.
41
+ func TestPath(p ...int) []int {
42
+ return p
43
+ }
44
+
45
+ // TestMatch looks for the element targeted by the descriptor in the given tree
46
+ // and reports whether it matches with the expected element.
47
+ //
48
+ // Eg:
49
+ // tree := app.Div().Body(
50
+ // app.H2().Body(
51
+ // app.Text("foo"),
52
+ // ),
53
+ // app.P().Body(
54
+ // app.Text("bar"),
55
+ // ),
56
+ // )
57
+ //
58
+ // // Testing root:
59
+ // err := app.TestMatch(tree, app.TestUIDescriptor{
60
+ // Path: TestPath(),
61
+ // Expected: app.Div(),
62
+ // })
63
+ // // OK => err == nil
64
+ //
65
+ // // Testing h2:
66
+ // err := app.TestMatch(tree, app.TestUIDescriptor{
67
+ // Path: TestPath(0),
68
+ // Expected: app.H3(),
69
+ // })
70
+ // // KO => err != nil because we ask h2 to match with h3
71
+ //
72
+ // // Testing text from p:
73
+ // err = app.TestMatch(tree, app.TestUIDescriptor{
74
+ // Path: TestPath(1, 0),
75
+ // Expected: app.Text("bar"),
76
+ // })
77
+ // // OK => err == nil
78
+ func TestMatch(tree UI, d TestUIDescriptor) error {
79
+ if !tree.Mounted() {
80
+ if err := mount(tree); err != nil {
81
+ return err
82
+ }
83
+ }
84
+
85
+ if d.Expected != nil {
86
+ d.Expected.setSelf(d.Expected)
87
+ }
88
+
89
+ if len(d.Path) != 0 {
90
+ idx := d.Path[0]
91
+
92
+ if idx < 0 || idx >= len(tree.children()) {
93
+ // Check that the element does not exists.
94
+ if d.Expected == nil {
95
+ return nil
96
+ }
97
+
98
+ return errors.New("ui element to match is out of range").
99
+ Tag("name", d.Expected.name()).
100
+ Tag("parent-name", tree.name()).
101
+ Tag("parent-children-count", len(tree.children())).
102
+ Tag("index", idx)
103
+ }
104
+
105
+ c := tree.children()[idx]
106
+ p := c.parent()
107
+
108
+ if p != tree {
109
+ return errors.New("unexpected ui element parent").
110
+ Tag("name", d.Expected.name()).
111
+ Tag("parent-name", p.name()).
112
+ Tag("parent-addr", fmt.Sprintf("%p", p)).
113
+ Tag("expected-parent-name", tree.name()).
114
+ Tag("expected-parent-addr", fmt.Sprintf("%p", tree))
115
+ }
116
+
117
+ d.Path = d.Path[1:]
118
+ return TestMatch(c, d)
119
+ }
120
+
121
+ if d.Expected.name() != tree.name() {
122
+ return errors.New("the UI element is not matching the descriptor").
123
+ Tag("expected-name", d.Expected.name()).
124
+ Tag("current-name", tree.name())
125
+ }
126
+
127
+ // switch d.Expected.Kind() {
128
+ // case SimpleText:
129
+ // return matchText(tree, d)
130
+
131
+ // case HTML:
132
+ // if err := matchHTMLElemAttrs(tree, d); err != nil {
133
+ // return err
134
+ // }
135
+ // return matchHTMLElemEventHandlers(tree, d)
136
+
137
+ // // case Component:
138
+ // // return matchComponent(tree, d)
139
+
140
+ // case RawHTML:
141
+ // return matchRaw(tree, d)
142
+
143
+ // default:
144
+ // return errors.New("the UI element is not matching the descriptor").
145
+ // Tag("reason", "unavailable matching for the kind").
146
+ // Tag("kind", d.Expected.Kind())
147
+ // }
148
+ return nil
149
+ }
144
150
 
145
151
  // func matchText(n UI, d TestUIDescriptor) error {
146
152
  // a := n.(*text)
testing_test.go CHANGED
@@ -1,5 +1,10 @@
1
1
  package app
2
2
 
3
+ import (
4
+ "runtime"
5
+ "testing"
6
+ )
7
+
3
8
  // import (
4
9
  // "io/ioutil"
5
10
  // "os"
@@ -9,16 +14,11 @@ package app
9
14
  // "github.com/stretchr/testify/require"
10
15
  // )
11
16
 
12
- // func testSkipNonWasm(t *testing.T) {
17
+ func testSkipNonWasm(t *testing.T) {
13
- // if goarch := runtime.GOARCH; goarch != "wasm" {
18
+ if goarch := runtime.GOARCH; goarch != "wasm" {
14
- // t.Skip("skipping test")
19
+ t.Skip("skipping test")
15
- // // t.Skip(logs.New("skipping test").
16
- // // Tag("reason", "unsupported architecture").
17
- // // Tag("required-architecture", "wasm").
18
- // // Tag("current-architecture", goarch),
19
- // // )
20
- // }
21
- // }
20
+ }
21
+ }
22
22
 
23
23
  // func testSkipWasm(t *testing.T) {
24
24
  // if goarch := runtime.GOARCH; goarch == "wasm" {