experimental: renderer
All checks were successful
NeoVim tests / plenary-tests (push) Successful in 49s

This commit is contained in:
2025-02-19 23:16:26 -07:00
parent de01a95cdc
commit 3a7cd66729
7 changed files with 1122 additions and 25 deletions

206
spec/tracker_spec.lua Normal file
View File

@@ -0,0 +1,206 @@
local tracker = require 'u.tracker'
local Signal = tracker.Signal
local ExecutionContext = tracker.ExecutionContext
describe('Signal', function()
local signal
before_each(function() signal = Signal:new(0, 'testSignal') end)
it('should initialize with correct parameters', function()
assert.is.equal(signal.value, 0)
assert.is.equal(signal.name, 'testSignal')
assert.is.not_nil(signal.subscribers)
assert.is.equal(#signal.subscribers, 0)
assert.is.equal(signal.changing, false)
end)
it('should set new value and notify subscribers', function()
local called = false
signal:subscribe(function(value)
called = true
assert.is.equal(value, 42)
end)
signal:set(42)
assert.is.equal(called, true)
end)
it('should not notify subscribers during circular dependency', function()
signal.changing = true
local notified = false
signal:subscribe(function() notified = true end)
signal:set(42)
assert.is.equal(notified, false) -- No notification should occur
end)
it('should get current value', function()
signal:set(100)
assert.is.equal(signal:get(), 100)
end)
it('should update value with function', function()
signal:set(10)
signal:update(function(value) return value * 2 end)
assert.is.equal(signal:get(), 20)
end)
it('should dispose subscribers', function()
local called = false
local unsubscribe = signal:subscribe(function() called = true end)
unsubscribe()
signal:set(10)
assert.is.equal(called, false) -- Should not be notified
end)
describe('Signal:map', function()
it('should transform the signal value', function()
local signal = Signal:new(5)
local mapped_signal = signal:map(function(value) return value * 2 end)
assert.is.equal(mapped_signal:get(), 10) -- Initial transformation
signal:set(10)
assert.is.equal(mapped_signal:get(), 20) -- Updated transformation
end)
it('should handle empty transformations', function()
local signal = Signal:new(nil)
local mapped_signal = signal:map(function(value) return value or 'default' end)
assert.is.equal(mapped_signal:get(), 'default') -- Return default
signal:set 'new value'
assert.is.equal(mapped_signal:get(), 'new value') -- Return new value
end)
end)
describe('Signal:filter', function()
it('should only emit values that pass the filter', function()
local signal = Signal:new(5)
local filtered_signal = signal:filter(function(value) return value > 10 end)
assert.is.equal(filtered_signal:get(), nil) -- Initial value should not pass
signal:set(15)
assert.is.equal(filtered_signal:get(), 15) -- Now filtered
signal:set(8)
assert.is.equal(filtered_signal:get(), 15) -- Does not pass the filter
end)
it('should handle empty initial values', function()
local signal = Signal:new(nil)
local filtered_signal = signal:filter(function(value) return value ~= nil end)
assert.is.equal(filtered_signal:get(), nil) -- Should be nil
signal:set(10)
assert.is.equal(filtered_signal:get(), 10) -- Should pass now
end)
end)
describe('create_memo', function()
it('should compute a derived value and update when dependencies change', function()
local signal = Signal:new(2)
local memoized_signal = tracker.create_memo(function() return signal:get() * 2 end)
assert.is.equal(memoized_signal:get(), 4) -- Initially compute 2 * 2
signal:set(3)
assert.is.equal(memoized_signal:get(), 6) -- Update to 3 * 2 = 6
signal:set(5)
assert.is.equal(memoized_signal:get(), 10) -- Update to 5 * 2 = 10
end)
it('should not recompute if the dependencies do not change', function()
local call_count = 0
local signal = Signal:new(10)
local memoized_signal = tracker.create_memo(function()
call_count = call_count + 1
return signal:get() + 1
end)
assert.is.equal(memoized_signal:get(), 11) -- Compute first value
assert.is.equal(call_count, 1) -- Should compute once
memoized_signal:get() -- Call again, should use memoized value
assert.is.equal(call_count, 1) -- Still should only be one call
signal:set(10) -- Set the same value
assert.is.equal(memoized_signal:get(), 11)
assert.is.equal(call_count, 2)
signal:set(20)
assert.is.equal(memoized_signal:get(), 21)
assert.is.equal(call_count, 3)
end)
end)
describe('create_effect', function()
it('should track changes and execute callback', function()
local signal = Signal:new(5)
local call_count = 0
tracker.create_effect(function()
signal:get() -- track as a dependency
call_count = call_count + 1
end)
assert.is.equal(call_count, 1)
signal:set(10)
assert.is.equal(call_count, 2)
end)
it('should clean up signals and not call after dispose', function()
local signal = Signal:new(5)
local call_count = 0
local unsubscribe = tracker.create_effect(function()
call_count = call_count + 1
return signal:get() * 2
end)
assert.is.equal(call_count, 1) -- Initially calls
unsubscribe() -- Unsubscribe the effect
signal:set(10) -- Update signal value
assert.is.equal(call_count, 1) -- Callback should not be called again
end)
end)
end)
describe('ExecutionContext', function()
local context
before_each(function() context = ExecutionContext:new() end)
it('should initialize a new context', function()
assert.is.table(context.signals)
assert.is.table(context.subscribers)
end)
it('should track signals', function()
local signal = Signal:new(0)
context:track(signal)
assert.is.equal(next(context.signals), signal) -- Check if signal is tracked
end)
it('should subscribe to signals', function()
local signal = Signal:new(0)
local callback_called = false
context:track(signal)
context:subscribe(function() callback_called = true end)
signal:set(100)
assert.is.equal(callback_called, true) -- Callback should be called
end)
it('should dispose tracked signals', function()
local signal = Signal:new(0)
context:track(signal)
context:dispose()
assert.is.falsy(next(context.signals)) -- Should not have any tracked signals
end)
end)

70
spec/utils_spec.lua Normal file
View File

@@ -0,0 +1,70 @@
local utils = require 'u.utils'
--- @param s string
local function split(s) return vim.split(s, '') end
--- @param original string
--- @param changes LevenshteinChange[]
local function morph(original, changes)
local t = split(original)
for _, change in ipairs(changes) do
if change.kind == 'add' then
table.insert(t, change.index, change.item)
elseif change.kind == 'delete' then
table.remove(t, change.index)
elseif change.kind == 'change' then
t[change.index] = change.to
end
end
return vim.iter(t):join ''
end
describe('utils', function()
it('levenshtein', function()
local original = 'abc'
local result = 'absece'
local changes = utils.levenshtein(split(original), split(result))
assert.are.same(changes, {
{
item = 'e',
kind = 'add',
index = 4,
},
{
item = 'e',
kind = 'add',
index = 3,
},
{
item = 's',
kind = 'add',
index = 3,
},
})
assert.are.same(morph(original, changes), result)
original = 'jonathan'
result = 'ajoanthan'
changes = utils.levenshtein(split(original), split(result))
assert.are.same(changes, {
{
from = 'a',
index = 4,
kind = 'change',
to = 'n',
},
{
from = 'n',
index = 3,
kind = 'change',
to = 'a',
},
{
index = 1,
item = 'a',
kind = 'add',
},
})
assert.are.same(morph(original, changes), result)
end)
end)