Files
ibrahim dursunandGitHub 80b1c64db3 feat(lua): make default actions overridable and expose jjui.builtin.* (#577)
* refactor(resolver): resolve lua based actions first

* feat(lua): add `builtin` namespace

This change defines `jjui.builtin` namespace so that they are still accessible even if the custom lua script overrides a default action. Handy for adding extra functionality around the existing actions without changing the bindings.

example:

```lua
  config.action("revisions.move_down", function()
    jjui.builtin.revisions.move_down()
    flash("down")
  end)
  config.action("revisions.refresh", function()
    jjui.builtin.revisions.refresh()
    jjui.wait_refresh()
    flash("refresh completed")
  end)
```

* refactor: don't jump to parent after toggling select

but instead use action override `revisions.toggle_select` to keep backwards compability
2026-03-04 02:58:44 +00:00

423 lines
11 KiB
Go

package scripting
import (
"testing"
lua "github.com/yuin/gopher-lua"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/idursun/jjui/internal/ui/common"
uicontext "github.com/idursun/jjui/internal/ui/context"
)
func strPtr(v string) *string {
return &v
}
func assertLuaStringOrNil(t *testing.T, val lua.LValue, expected *string) {
t.Helper()
if expected == nil {
assert.Equal(t, lua.LNil, val)
return
}
assert.Equal(t, *expected, val.String())
}
// runScriptAndGetGlobal runs a Lua script and returns the value of a global variable
// before the Lua state is closed.
func runScriptAndGetGlobal(t *testing.T, ctx *uicontext.MainContext, script, varName string) lua.LValue {
results := runScriptAndGetGlobals(t, ctx, script, varName)
if len(results) > 0 {
return results[0]
}
return lua.LNil
}
func runScriptAndGetGlobals(t *testing.T, ctx *uicontext.MainContext, script string, varNames ...string) []lua.LValue {
L := lua.NewState()
defer L.Close()
registerAPI(L, ctx)
fn, err := L.LoadString(script)
assert.NoError(t, err)
err = L.CallByParam(lua.P{
Fn: fn,
NRet: 0,
Protect: true,
})
assert.NoError(t, err)
var results []lua.LValue
for _, name := range varNames {
results = append(results, L.GetGlobal(name))
}
return results
}
func TestContext_ChangeId(t *testing.T) {
tests := []struct {
name string
ctx *uicontext.MainContext
want *string
}{
{
name: "selected revision",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
ChangeId: "abc123",
CommitId: "def456",
}},
want: strPtr("abc123"),
},
{
name: "selected file",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedFile{
ChangeId: "file123",
CommitId: "commit456",
File: "test.go",
}},
want: strPtr("file123"),
},
{
name: "no selection",
ctx: &uicontext.MainContext{},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val := runScriptAndGetGlobal(t, tt.ctx, `result = context.change_id()`, "result")
assertLuaStringOrNil(t, val, tt.want)
})
}
}
func TestContext_CommitId(t *testing.T) {
tests := []struct {
name string
ctx *uicontext.MainContext
want *string
}{
{
name: "selected revision",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
ChangeId: "abc123",
CommitId: "def456",
}},
want: strPtr("def456"),
},
{
name: "selected file",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedFile{
ChangeId: "file123",
CommitId: "commit456",
File: "test.go",
}},
want: strPtr("commit456"),
},
{
name: "selected commit",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedCommit{
CommitId: "onlycommit789",
}},
want: strPtr("onlycommit789"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val := runScriptAndGetGlobal(t, tt.ctx, `result = context.commit_id()`, "result")
assertLuaStringOrNil(t, val, tt.want)
})
}
}
func TestContext_File(t *testing.T) {
tests := []struct {
name string
ctx *uicontext.MainContext
want *string
}{
{
name: "selected file",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedFile{
ChangeId: "file123",
CommitId: "commit456",
File: "path/to/file.go",
}},
want: strPtr("path/to/file.go"),
},
{
name: "selected revision",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
ChangeId: "abc123",
CommitId: "def456",
}},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val := runScriptAndGetGlobal(t, tt.ctx, `result = context.file()`, "result")
assertLuaStringOrNil(t, val, tt.want)
})
}
}
func TestContext_OperationId(t *testing.T) {
tests := []struct {
name string
ctx *uicontext.MainContext
want *string
}{
{
name: "selected operation",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedOperation{
OperationId: "op123456",
}},
want: strPtr("op123456"),
},
{
name: "selected revision",
ctx: &uicontext.MainContext{SelectedItem: uicontext.SelectedRevision{
ChangeId: "abc123",
CommitId: "def456",
}},
want: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
val := runScriptAndGetGlobal(t, tt.ctx, `result = context.operation_id()`, "result")
assertLuaStringOrNil(t, val, tt.want)
})
}
}
func TestContext_CheckedFiles(t *testing.T) {
ctx := &uicontext.MainContext{
CheckedItems: []uicontext.SelectedItem{
uicontext.SelectedFile{ChangeId: "c1", CommitId: "co1", File: "file1.go"},
uicontext.SelectedFile{ChangeId: "c2", CommitId: "co2", File: "file2.go"},
uicontext.SelectedRevision{ChangeId: "rev1", CommitId: "com1"}, // should be ignored
uicontext.SelectedFile{ChangeId: "c3", CommitId: "co3", File: "file3.go"},
},
}
vals := runScriptAndGetGlobals(t, ctx, `
files = context.checked_files()
count = #files
first = files[1]
second = files[2]
third = files[3]
`, "count", "first", "second", "third")
assert.Equal(t, lua.LNumber(3), vals[0])
assert.Equal(t, "file1.go", vals[1].String())
assert.Equal(t, "file2.go", vals[2].String())
assert.Equal(t, "file3.go", vals[3].String())
}
func TestContext_CheckedFiles_Empty(t *testing.T) {
ctx := &uicontext.MainContext{
CheckedItems: []uicontext.SelectedItem{},
}
val := runScriptAndGetGlobal(t, ctx, `
files = context.checked_files()
result = #files
`, "result")
assert.Equal(t, lua.LNumber(0), val)
}
func TestContext_CheckedChangeIds(t *testing.T) {
ctx := &uicontext.MainContext{
CheckedItems: []uicontext.SelectedItem{
uicontext.SelectedRevision{ChangeId: "change1", CommitId: "com1"},
uicontext.SelectedFile{ChangeId: "change2", CommitId: "com2", File: "f.go"},
uicontext.SelectedOperation{OperationId: "op1"}, // should be ignored
uicontext.SelectedRevision{ChangeId: "change3", CommitId: "com3"},
},
}
vals := runScriptAndGetGlobals(t, ctx, `
ids = context.checked_change_ids()
count = #ids
first = ids[1]
second = ids[2]
third = ids[3]
`, "count", "first", "second", "third")
assert.Equal(t, lua.LNumber(3), vals[0])
assert.Equal(t, "change1", vals[1].String())
assert.Equal(t, "change2", vals[2].String())
assert.Equal(t, "change3", vals[3].String())
}
func TestContext_CheckedCommitIds(t *testing.T) {
ctx := &uicontext.MainContext{
CheckedItems: []uicontext.SelectedItem{
uicontext.SelectedRevision{ChangeId: "c1", CommitId: "commit1"},
uicontext.SelectedFile{ChangeId: "c2", CommitId: "commit2", File: "f.go"},
uicontext.SelectedCommit{CommitId: "commit3"},
uicontext.SelectedOperation{OperationId: "op1"}, // should be ignored
},
}
vals := runScriptAndGetGlobals(t, ctx, `
ids = context.checked_commit_ids()
count = #ids
first = ids[1]
second = ids[2]
third = ids[3]
`, "count", "first", "second", "third")
assert.Equal(t, lua.LNumber(3), vals[0])
assert.Equal(t, "commit1", vals[1].String())
assert.Equal(t, "commit2", vals[2].String())
assert.Equal(t, "commit3", vals[3].String())
}
func TestContext_AccessViaJjuiNamespace(t *testing.T) {
ctx := &uicontext.MainContext{
SelectedItem: uicontext.SelectedRevision{
ChangeId: "ns_change",
CommitId: "ns_commit",
},
}
vals := runScriptAndGetGlobals(t, ctx, `
change = jjui.context.change_id()
commit = jjui.context.commit_id()
`, "change", "commit")
assert.Equal(t, "ns_change", vals[0].String())
assert.Equal(t, "ns_commit", vals[1].String())
}
func TestGeneratedActions_AccessViaJjuiNamespace(t *testing.T) {
ctx := &uicontext.MainContext{}
vals := runScriptAndGetGlobals(t, ctx, `
result_details = type(jjui.revisions.details.cancel)
result_nested = type(jjui.revisions.details.confirmation.apply)
result_builtin = type(jjui.builtin.revisions.details.cancel)
result_legacy = type(jjui.action)
`, "result_details", "result_nested", "result_builtin", "result_legacy")
assert.Equal(t, "function", vals[0].String())
assert.Equal(t, "function", vals[1].String())
assert.Equal(t, "function", vals[2].String())
assert.Equal(t, "nil", vals[3].String())
}
func TestWaitHelpers_AccessViaTopLevelAndJjuiNamespace(t *testing.T) {
ctx := &uicontext.MainContext{}
vals := runScriptAndGetGlobals(t, ctx, `
top_close = type(wait_close)
top_refresh = type(wait_refresh)
ns_close = type(jjui.wait_close)
ns_refresh = type(jjui.wait_refresh)
`, "top_close", "top_refresh", "ns_close", "ns_refresh")
assert.Equal(t, "function", vals[0].String())
assert.Equal(t, "function", vals[1].String())
assert.Equal(t, "function", vals[2].String())
assert.Equal(t, "function", vals[3].String())
}
func runWaitingScript(t *testing.T, script string) (*uicontext.MainContext, *Runner) {
t.Helper()
ctx := setupVM(t)
runner, cmd, err := RunScript(ctx, script)
require.NoError(t, err)
require.NotNil(t, runner)
if cmd != nil {
_ = cmd()
}
require.False(t, runner.Done())
return ctx, runner
}
func TestRunner_WaitClose(t *testing.T) {
t.Run("ignores non-close messages and resumes on close", func(t *testing.T) {
ctx, runner := runWaitingScript(t, `
done = false
applied = wait_close()
done = true
`)
cmd := runner.HandleMsg(common.UpdateRevisionsSuccessMsg{})
assert.Nil(t, cmd)
assert.False(t, runner.Done())
assert.Equal(t, "false", ctx.ScriptVM.GetGlobal("done").String())
cmd = runner.HandleMsg(common.CloseViewMsg{Applied: true})
if cmd != nil {
_ = cmd()
}
assert.True(t, runner.Done())
assert.Equal(t, "true", ctx.ScriptVM.GetGlobal("done").String())
assert.Equal(t, "true", ctx.ScriptVM.GetGlobal("applied").String())
})
tests := []struct {
name string
applied bool
}{
{name: "applied", applied: true},
{name: "cancelled", applied: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, runner := runWaitingScript(t, `applied = wait_close()`)
cmd := runner.HandleMsg(common.CloseViewMsg{Applied: tt.applied})
if cmd != nil {
_ = cmd()
}
assert.True(t, runner.Done())
assert.Equal(t, tt.applied, ctx.ScriptVM.GetGlobal("applied") == lua.LTrue)
})
}
}
func TestRunner_WaitRefreshResumesOnUpdateRevisions(t *testing.T) {
tests := []struct {
name string
msg any
}{
{name: "success", msg: common.UpdateRevisionsSuccessMsg{}},
{name: "failed", msg: common.UpdateRevisionsFailedMsg{Output: "err"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, runner := runWaitingScript(t, `
done = false
wait_refresh()
done = true
`)
cmd := runner.HandleMsg(tt.msg)
if cmd != nil {
_ = cmd()
}
assert.True(t, runner.Done())
assert.Equal(t, "true", ctx.ScriptVM.GetGlobal("done").String())
})
}
}