sqs-browse: started working on tests for controllers
This commit is contained in:
parent
fb749aaee2
commit
43680000a8
2
go.mod
2
go.mod
|
@ -5,6 +5,7 @@ go 1.17
|
|||
require (
|
||||
github.com/aws/aws-sdk-go-v2 v1.15.0
|
||||
github.com/aws/aws-sdk-go-v2/config v1.13.1
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0
|
||||
github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.8.0
|
||||
github.com/aws/aws-sdk-go-v2/service/dynamodb v1.15.0
|
||||
github.com/aws/aws-sdk-go-v2/service/sqs v1.16.0
|
||||
|
@ -20,7 +21,6 @@ require (
|
|||
|
||||
require (
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.8.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.10.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.0 // indirect
|
||||
|
|
|
@ -7,24 +7,25 @@ import (
|
|||
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||
)
|
||||
|
||||
type dispatcherContext struct {
|
||||
d *Dispatcher
|
||||
type DispatcherContext struct {
|
||||
Publisher MessagePublisher
|
||||
}
|
||||
|
||||
func (dc dispatcherContext) Messagef(format string, args ...interface{}) {
|
||||
dc.d.publisher.Send(events.Message(fmt.Sprintf(format, args...)))
|
||||
func (dc DispatcherContext) Messagef(format string, args ...interface{}) {
|
||||
dc.Publisher.Send(events.Message(fmt.Sprintf(format, args...)))
|
||||
}
|
||||
|
||||
func (dc dispatcherContext) Send(teaMessage tea.Msg) {
|
||||
dc.d.publisher.Send(teaMessage)
|
||||
func (dc DispatcherContext) Send(teaMessage tea.Msg) {
|
||||
dc.Publisher.Send(teaMessage)
|
||||
}
|
||||
|
||||
func (dc dispatcherContext) Message(msg string) {
|
||||
dc.d.publisher.Send(events.Message(msg))
|
||||
func (dc DispatcherContext) Message(msg string) {
|
||||
dc.Publisher.Send(events.Message(msg))
|
||||
}
|
||||
|
||||
func (dc dispatcherContext) Input(prompt string, onDone uimodels.Operation) {
|
||||
dc.d.publisher.Send(events.PromptForInput{
|
||||
func (dc DispatcherContext) Input(prompt string, onDone uimodels.Operation) {
|
||||
dc.Publisher.Send(events.PromptForInput{
|
||||
Prompt: prompt,
|
||||
OnDone: onDone,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -32,7 +32,7 @@ func (d *Dispatcher) Start(ctx context.Context, operation uimodels.Operation) {
|
|||
|
||||
d.runningOp = operation
|
||||
go func() {
|
||||
subCtx := uimodels.WithContext(ctx, dispatcherContext{d})
|
||||
subCtx := uimodels.WithContext(ctx, DispatcherContext{d.publisher})
|
||||
|
||||
err := operation.Execute(subCtx)
|
||||
if err != nil {
|
||||
|
|
|
@ -12,5 +12,6 @@ type Message string
|
|||
|
||||
// PromptForInput indicates that the context is requesting a line of input
|
||||
type PromptForInput struct {
|
||||
Prompt string
|
||||
OnDone uimodels.Operation
|
||||
}
|
|
@ -5,3 +5,7 @@ import "github.com/lmika/awstools/internal/dynamo-browse/models"
|
|||
type NewResultSet struct {
|
||||
ResultSet *models.ResultSet
|
||||
}
|
||||
|
||||
type SetReadWrite struct {
|
||||
NewValue bool
|
||||
}
|
27
internal/dynamo-browse/controllers/state.go
Normal file
27
internal/dynamo-browse/controllers/state.go
Normal file
|
@ -0,0 +1,27 @@
|
|||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
)
|
||||
|
||||
type State struct {
|
||||
ResultSet *models.ResultSet
|
||||
SelectedItem models.Item
|
||||
|
||||
// InReadWriteMode indicates whether modifications can be made to the table
|
||||
InReadWriteMode bool
|
||||
}
|
||||
|
||||
type stateContextKeyType struct{}
|
||||
|
||||
var stateContextKey = stateContextKeyType{}
|
||||
|
||||
func CurrentState(ctx context.Context) State {
|
||||
state, _ := ctx.Value(stateContextKey).(State)
|
||||
return state
|
||||
}
|
||||
|
||||
func ContextWithState(ctx context.Context, state State) context.Context {
|
||||
return context.WithValue(ctx, stateContextKey, state)
|
||||
}
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
@ -22,11 +21,26 @@ func NewTableWriteController(tableService *tables.Service, tableReadControllers
|
|||
}
|
||||
}
|
||||
|
||||
func (c *TableWriteController) Delete(item models.Item) uimodels.Operation {
|
||||
func (c *TableWriteController) EnableReadWrite() uimodels.Operation {
|
||||
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||
uiCtx := uimodels.Ctx(ctx)
|
||||
uiCtx.Send(SetReadWrite{NewValue: true})
|
||||
uiCtx.Message("read/write mode enabled")
|
||||
|
||||
// TODO: only do if rw mode enabled
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (c *TableWriteController) Delete() uimodels.Operation {
|
||||
return uimodels.OperationFn(func(ctx context.Context) error {
|
||||
uiCtx := uimodels.Ctx(ctx)
|
||||
state := CurrentState(ctx)
|
||||
|
||||
if state.SelectedItem == nil {
|
||||
return errors.New("no selected item")
|
||||
} else if !state.InReadWriteMode {
|
||||
return errors.New("not in read/write mode")
|
||||
}
|
||||
|
||||
uiCtx.Input("Delete item? ", uimodels.OperationFn(func(ctx context.Context) error {
|
||||
uiCtx := uimodels.Ctx(ctx)
|
||||
|
@ -36,7 +50,7 @@ func (c *TableWriteController) Delete(item models.Item) uimodels.Operation {
|
|||
}
|
||||
|
||||
// Delete the item
|
||||
if err := c.tableService.Delete(ctx, c.tableName, item); err != nil {
|
||||
if err := c.tableService.Delete(ctx, c.tableName, state.SelectedItem); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
|
165
internal/dynamo-browse/controllers/tablewrite_test.go
Normal file
165
internal/dynamo-browse/controllers/tablewrite_test.go
Normal file
|
@ -0,0 +1,165 @@
|
|||
package controllers_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/providers/dynamo"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/services/tables"
|
||||
"github.com/lmika/awstools/test/testdynamo"
|
||||
"github.com/lmika/awstools/test/testuictx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTableWriteController_EnableReadWrite(t *testing.T) {
|
||||
twc, _, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
|
||||
t.Run("should send event enabling read write", func(t *testing.T) {
|
||||
ctx, uiCtx := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
InReadWriteMode: false,
|
||||
})
|
||||
|
||||
err := twc.EnableReadWrite().Execute(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Contains(t, uiCtx.Messages, controllers.SetReadWrite{NewValue: true})
|
||||
})
|
||||
}
|
||||
|
||||
func TestTableWriteController_Delete(t *testing.T) {
|
||||
t.Run("should delete selected item if in read/write mode is inactive", func(t *testing.T) {
|
||||
twc, ctrls, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
|
||||
resultSet, err := ctrls.tableService.Scan(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resultSet.Items, 3)
|
||||
|
||||
ctx, uiCtx := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
ResultSet: resultSet,
|
||||
SelectedItem: resultSet.Items[1],
|
||||
InReadWriteMode: false,
|
||||
})
|
||||
|
||||
op := twc.Delete()
|
||||
|
||||
// Should prompt first
|
||||
err = op.Execute(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
promptRequest, ok := uiCtx.Messages[0].(events.PromptForInput)
|
||||
assert.True(t, ok)
|
||||
|
||||
// After prompt, continue to delete
|
||||
err = promptRequest.OnDone.Execute(uimodels.WithPromptValue(ctx, "y"))
|
||||
assert.NoError(t, err)
|
||||
|
||||
afterResultSet, err := ctrls.tableService.Scan(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, afterResultSet.Items, 2)
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[0])
|
||||
assert.NotContains(t, afterResultSet.Items, resultSet.Items[1])
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[2])
|
||||
})
|
||||
|
||||
t.Run("should not delete selected item if prompt is not y", func(t *testing.T) {
|
||||
twc, ctrls, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
|
||||
resultSet, err := ctrls.tableService.Scan(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resultSet.Items, 3)
|
||||
|
||||
ctx, uiCtx := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
ResultSet: resultSet,
|
||||
SelectedItem: resultSet.Items[1],
|
||||
InReadWriteMode: false,
|
||||
})
|
||||
|
||||
op := twc.Delete()
|
||||
|
||||
// Should prompt first
|
||||
err = op.Execute(ctx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
promptRequest, ok := uiCtx.Messages[0].(events.PromptForInput)
|
||||
assert.True(t, ok)
|
||||
|
||||
// After prompt, continue to delete
|
||||
err = promptRequest.OnDone.Execute(uimodels.WithPromptValue(ctx, "n"))
|
||||
assert.Error(t, err)
|
||||
|
||||
afterResultSet, err := ctrls.tableService.Scan(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, afterResultSet.Items, 3)
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[0])
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[1])
|
||||
assert.Contains(t, afterResultSet.Items, resultSet.Items[2])
|
||||
})
|
||||
|
||||
t.Run("should not delete if read/write mode is inactive", func(t *testing.T) {
|
||||
tableWriteController, ctrls, closeFn := setupController(t)
|
||||
t.Cleanup(closeFn)
|
||||
|
||||
resultSet, err := ctrls.tableService.Scan(context.Background(), ctrls.tableName)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, resultSet.Items, 3)
|
||||
|
||||
ctx, _ := testuictx.New(context.Background())
|
||||
ctx = controllers.ContextWithState(ctx, controllers.State{
|
||||
ResultSet: resultSet,
|
||||
SelectedItem: resultSet.Items[1],
|
||||
InReadWriteMode: false,
|
||||
})
|
||||
|
||||
op := tableWriteController.Delete()
|
||||
|
||||
err = op.Execute(ctx)
|
||||
assert.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
type controller struct {
|
||||
tableName string
|
||||
tableService *tables.Service
|
||||
}
|
||||
|
||||
func setupController(t *testing.T) (*controllers.TableWriteController, controller, func()) {
|
||||
tableName := "table-write-controller-table"
|
||||
|
||||
client, cleanupFn := testdynamo.SetupTestTable(t, tableName, testData)
|
||||
provider := dynamo.NewProvider(client)
|
||||
tableService := tables.NewService(provider)
|
||||
tableReadController := controllers.NewTableReadController(tableService, tableName)
|
||||
tableWriteController := controllers.NewTableWriteController(tableService, tableReadController, tableName)
|
||||
return tableWriteController, controller{
|
||||
tableName: tableName,
|
||||
tableService: tableService,
|
||||
}, cleanupFn
|
||||
}
|
||||
|
||||
var testData = testdynamo.TestData{
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "222",
|
||||
"alpha": "This is another some value",
|
||||
"beta": 1231,
|
||||
},
|
||||
{
|
||||
"pk": "abc",
|
||||
"sk": "111",
|
||||
"alpha": "This is some value",
|
||||
},
|
||||
{
|
||||
"pk": "bbb",
|
||||
"sk": "131",
|
||||
"beta": 2468,
|
||||
"gamma": "foobar",
|
||||
},
|
||||
}
|
|
@ -3,6 +3,7 @@ package models
|
|||
import "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
|
||||
type ResultSet struct {
|
||||
Table string
|
||||
Columns []string
|
||||
Items []Item
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ func (s *Service) Scan(ctx context.Context, table string) (*models.ResultSet, er
|
|||
models.Sort(results, pk, sk)
|
||||
|
||||
return &models.ResultSet{
|
||||
Table: table,
|
||||
Columns: columns,
|
||||
Items: results,
|
||||
}, nil
|
||||
|
|
|
@ -24,6 +24,7 @@ func TestService_Scan(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
|
||||
// Hash first, then range, then columns in alphabetic order
|
||||
assert.Equal(t, rs.Table, tableName)
|
||||
assert.Equal(t, rs.Columns, []string{"pk", "sk", "alpha", "beta", "gamma"})
|
||||
assert.Equal(t, rs.Items[0], testdynamo.TestRecordAsItem(t, testData[1]))
|
||||
assert.Equal(t, rs.Items[1], testdynamo.TestRecordAsItem(t, testData[0]))
|
||||
|
|
|
@ -13,16 +13,19 @@ import (
|
|||
"github.com/lmika/awstools/internal/common/ui/events"
|
||||
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/controllers"
|
||||
"github.com/lmika/awstools/internal/dynamo-browse/models"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
)
|
||||
|
||||
var (
|
||||
headerStyle = lipgloss.NewStyle().
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#4479ff"))
|
||||
|
||||
inactiveHeaderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1"))
|
||||
)
|
||||
|
||||
type uiModel struct {
|
||||
|
@ -32,7 +35,8 @@ type uiModel struct {
|
|||
tableWidth, tableHeight int
|
||||
|
||||
ready bool
|
||||
resultSet *models.ResultSet
|
||||
//resultSet *models.ResultSet
|
||||
state controllers.State
|
||||
message string
|
||||
|
||||
pendingInput *events.PromptForInput
|
||||
|
@ -72,10 +76,11 @@ func (m *uiModel) updateTable() {
|
|||
return
|
||||
}
|
||||
|
||||
newTbl := table.New(m.resultSet.Columns, m.tableWidth, m.tableHeight)
|
||||
newRows := make([]table.Row, len(m.resultSet.Items))
|
||||
for i, r := range m.resultSet.Items {
|
||||
newRows[i] = itemTableRow{m.resultSet, r}
|
||||
resultSet := m.state.ResultSet
|
||||
newTbl := table.New(resultSet.Columns, m.tableWidth, m.tableHeight)
|
||||
newRows := make([]table.Row, len(resultSet.Items))
|
||||
for i, r := range resultSet.Items {
|
||||
newRows[i] = itemTableRow{resultSet, r}
|
||||
}
|
||||
newTbl.SetRows(newRows)
|
||||
|
||||
|
@ -83,7 +88,8 @@ func (m *uiModel) updateTable() {
|
|||
}
|
||||
|
||||
func (m *uiModel) selectedItem() (itemTableRow, bool) {
|
||||
if m.ready && m.resultSet != nil && len(m.resultSet.Items) > 0 {
|
||||
resultSet := m.state.ResultSet
|
||||
if m.ready && resultSet != nil && len(resultSet.Items) > 0 {
|
||||
selectedItem, ok := m.table.SelectedRow().(itemTableRow)
|
||||
if ok {
|
||||
return selectedItem, true
|
||||
|
@ -126,9 +132,11 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
// Local events
|
||||
case controllers.NewResultSet:
|
||||
m.resultSet = msg.ResultSet
|
||||
m.state.ResultSet = msg.ResultSet
|
||||
m.updateTable()
|
||||
m.updateViewportToSelectedMessage()
|
||||
case controllers.SetReadWrite:
|
||||
m.state.InReadWriteMode = msg.NewValue
|
||||
|
||||
// Shared events
|
||||
case events.Error:
|
||||
|
@ -136,6 +144,7 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
case events.Message:
|
||||
m.message = string(msg)
|
||||
case events.PromptForInput:
|
||||
m.textInput.Prompt = msg.Prompt
|
||||
m.textInput.Focus()
|
||||
m.textInput.SetValue("")
|
||||
m.pendingInput = &msg
|
||||
|
@ -185,11 +194,11 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
|
||||
// TODO: these should be moved somewhere else
|
||||
case "s":
|
||||
m.dispatcher.Start(context.Background(), m.tableReadController.Scan())
|
||||
m.invokeOperation(m.tableReadController.Scan())
|
||||
case "D":
|
||||
if selectedItem, ok := m.selectedItem(); ok {
|
||||
m.dispatcher.Start(context.Background(), m.tableWriteController.Delete(selectedItem.item))
|
||||
}
|
||||
m.invokeOperation(m.tableWriteController.Delete())
|
||||
case "w":
|
||||
m.invokeOperation(m.tableWriteController.EnableReadWrite())
|
||||
}
|
||||
default:
|
||||
m.textInput, textInputCommands = m.textInput.Update(msg)
|
||||
|
@ -204,6 +213,16 @@ func (m uiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
|||
return m, tea.Batch(textInputCommands, tableMsgs, viewportMsgs)
|
||||
}
|
||||
|
||||
func (m uiModel) invokeOperation(op uimodels.Operation) {
|
||||
state := m.state
|
||||
if selectedItem, ok := m.selectedItem(); ok {
|
||||
state.SelectedItem = selectedItem.item
|
||||
}
|
||||
|
||||
ctx := controllers.ContextWithState(context.Background(), state)
|
||||
m.dispatcher.Start(ctx, op)
|
||||
}
|
||||
|
||||
func (m uiModel) View() string {
|
||||
if !m.ready {
|
||||
return "Initializing"
|
||||
|
@ -229,14 +248,21 @@ func (m uiModel) View() string {
|
|||
}
|
||||
|
||||
func (m uiModel) headerView() string {
|
||||
title := headerStyle.Render("Table: XXX")
|
||||
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
var titleText string
|
||||
if m.state.ResultSet != nil {
|
||||
titleText = "Table: " + m.state.ResultSet.Table
|
||||
} else {
|
||||
titleText = "No table"
|
||||
}
|
||||
|
||||
title := activeHeaderStyle.Render(titleText)
|
||||
line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
||||
|
||||
func (m uiModel) splitterView() string {
|
||||
title := headerStyle.Render("Item")
|
||||
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
title := inactiveHeaderStyle.Render("Item")
|
||||
line := inactiveHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
||||
|
||||
|
|
|
@ -17,10 +17,14 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
headerStyle = lipgloss.NewStyle().
|
||||
activeHeaderStyle = lipgloss.NewStyle().
|
||||
Bold(true).
|
||||
Foreground(lipgloss.Color("#ffffff")).
|
||||
Background(lipgloss.Color("#eac610"))
|
||||
|
||||
inactiveHeaderStyle = lipgloss.NewStyle().
|
||||
Foreground(lipgloss.Color("#000000")).
|
||||
Background(lipgloss.Color("#d1d1d1"))
|
||||
)
|
||||
|
||||
type uiModel struct {
|
||||
|
@ -194,14 +198,14 @@ func (m uiModel) View() string {
|
|||
}
|
||||
|
||||
func (m uiModel) headerView() string {
|
||||
title := headerStyle.Render("Queue: XXX")
|
||||
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
title := activeHeaderStyle.Render("Queue: XXX")
|
||||
line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
||||
|
||||
func (m uiModel) splitterView() string {
|
||||
title := headerStyle.Render("Message")
|
||||
line := headerStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
title := activeHeaderStyle.Render("Message")
|
||||
line := activeHeaderStyle.Render(strings.Repeat(" ", max(0, m.viewport.Width-lipgloss.Width(title))))
|
||||
return lipgloss.JoinHorizontal(lipgloss.Left, title, line)
|
||||
}
|
||||
|
||||
|
|
21
test/testuictx/testuictx.go
Normal file
21
test/testuictx/testuictx.go
Normal file
|
@ -0,0 +1,21 @@
|
|||
package testuictx
|
||||
|
||||
import (
|
||||
"context"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/lmika/awstools/internal/common/ui/dispatcher"
|
||||
"github.com/lmika/awstools/internal/common/ui/uimodels"
|
||||
)
|
||||
|
||||
func New(ctx context.Context) (context.Context, *TestUIContext) {
|
||||
td := &TestUIContext{}
|
||||
return uimodels.WithContext(ctx, dispatcher.DispatcherContext{td}), td
|
||||
}
|
||||
|
||||
type TestUIContext struct {
|
||||
Messages []tea.Msg
|
||||
}
|
||||
|
||||
func (t *TestUIContext) Send(msg tea.Msg) {
|
||||
t.Messages = append(t.Messages, msg)
|
||||
}
|
Loading…
Reference in a new issue