Skip to content

Commit 12adc8f

Browse files
committed
test: add more unit tests for Atom functionalities
1 parent c913c67 commit 12adc8f

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed

packages/atom/test/Atom.test.ts

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,6 +1348,294 @@ describe("Atom", () => {
13481348
const result = r.get(atom)
13491349
expect(Result.isInterrupted(result)).toBeTruthy()
13501350
})
1351+
1352+
it("map", () => {
1353+
const r = Registry.make()
1354+
const state = Atom.make(1)
1355+
const doubled = Atom.map(state, (n) => n * 2)
1356+
1357+
expect(r.get(doubled)).toBe(2)
1358+
r.set(state, 5)
1359+
expect(r.get(doubled)).toBe(10)
1360+
})
1361+
1362+
it("mapResult", () => {
1363+
const r = Registry.make()
1364+
const effect = Atom.make(Effect.succeed(42))
1365+
const mapped = Atom.mapResult(effect, (n) => n * 2)
1366+
1367+
const result = r.get(mapped)
1368+
expect(Result.isSuccess(result)).toBe(true)
1369+
if (Result.isSuccess(result)) {
1370+
expect(result.value).toBe(84)
1371+
}
1372+
})
1373+
1374+
it("transform", () => {
1375+
const r = Registry.make()
1376+
const state = Atom.make(1)
1377+
const derived = Atom.transform(state, (get) => get(state) * 2 + 1)
1378+
1379+
expect(r.get(derived)).toBe(3)
1380+
r.set(state, 5)
1381+
expect(r.get(derived)).toBe(11)
1382+
})
1383+
1384+
it("debounce", async () => {
1385+
const r = Registry.make()
1386+
const state = Atom.make(0)
1387+
const debounced = Atom.debounce(state, "100 millis")
1388+
1389+
// Mount the debounced atom first
1390+
const unmount = r.mount(debounced)
1391+
1392+
let updates = 0
1393+
const cancel = r.subscribe(debounced, () => updates++)
1394+
1395+
r.set(state, 1)
1396+
r.set(state, 2)
1397+
r.set(state, 3)
1398+
1399+
expect(updates).toBe(0) // Should not update immediately
1400+
1401+
await vitest.advanceTimersByTimeAsync(150)
1402+
expect(r.get(debounced)).toBe(3)
1403+
expect(updates).toBe(1)
1404+
1405+
cancel()
1406+
unmount()
1407+
})
1408+
1409+
it("writable direct", () => {
1410+
const r = Registry.make()
1411+
let value = 0
1412+
1413+
const atom = Atom.writable(
1414+
() => value,
1415+
(_, newValue: number) => {
1416+
value = newValue
1417+
}
1418+
)
1419+
1420+
expect(r.get(atom)).toBe(0)
1421+
r.set(atom, 42)
1422+
r.refresh(atom) // Need to refresh to see the new value
1423+
expect(r.get(atom)).toBe(42)
1424+
expect(value).toBe(42)
1425+
})
1426+
1427+
it("readable direct", () => {
1428+
const r = Registry.make()
1429+
let value = 0
1430+
1431+
const atom = Atom.readable(() => value)
1432+
1433+
expect(r.get(atom)).toBe(0)
1434+
value = 42
1435+
r.refresh(atom) // Need to refresh to see the new value
1436+
expect(r.get(atom)).toBe(42)
1437+
})
1438+
1439+
it("effect failure", () => {
1440+
const r = Registry.make()
1441+
const failing = Atom.make(Effect.fail("error"))
1442+
1443+
const result = r.get(failing)
1444+
expect(Result.isFailure(result)).toBe(true)
1445+
if (Result.isFailure(result)) {
1446+
expect(Cause.isFailType(result.cause) && result.cause.error).toBe("error")
1447+
}
1448+
})
1449+
1450+
it("effect failure with previousSuccess", () => {
1451+
const r = Registry.make()
1452+
const atom = Atom.fn((shouldFail: boolean) => shouldFail ? Effect.fail("error") : Effect.succeed(42))
1453+
// First success
1454+
r.set(atom, false)
1455+
let result = r.get(atom)
1456+
expect(Result.isSuccess(result)).toBe(true)
1457+
if (Result.isSuccess(result)) {
1458+
expect(result.value).toBe(42)
1459+
}
1460+
1461+
// Then failure - should keep previous success
1462+
r.set(atom, true)
1463+
result = r.get(atom)
1464+
expect(Result.isFailure(result)).toBe(true)
1465+
const value = Result.value(result)
1466+
expect(Option.isSome(value)).toBe(true)
1467+
if (Option.isSome(value)) {
1468+
expect(value.value).toBe(42)
1469+
}
1470+
})
1471+
1472+
it("context.once", () => {
1473+
const r = Registry.make()
1474+
const state = Atom.make(1)
1475+
let getCount = 0
1476+
1477+
const derived = Atom.make((get) => {
1478+
getCount++
1479+
return get.once(state) * 2
1480+
})
1481+
1482+
expect(r.get(derived)).toBe(2)
1483+
expect(getCount).toBe(1)
1484+
1485+
// Should not trigger rebuild on state change since we used once
1486+
r.set(state, 5)
1487+
expect(r.get(derived)).toBe(2) // Still old value
1488+
expect(getCount).toBe(1) // No rebuild
1489+
})
1490+
1491+
it("context.mount", () => {
1492+
const r = Registry.make()
1493+
let mounted = false
1494+
let unmounted = false
1495+
1496+
const atom = Atom.make((get) => {
1497+
const other = Atom.make(Effect.sync(() => {
1498+
mounted = true
1499+
return "value"
1500+
}))
1501+
1502+
get.addFinalizer(() => {
1503+
unmounted = true
1504+
})
1505+
1506+
get.mount(other)
1507+
return get(other)
1508+
})
1509+
1510+
const result = r.get(atom)
1511+
expect(Result.isSuccess(result)).toBe(true)
1512+
expect(mounted).toBe(true)
1513+
expect(unmounted).toBe(false)
1514+
})
1515+
1516+
it("context.refresh", () => {
1517+
const r = Registry.make()
1518+
let counter = 0
1519+
const state = Atom.make(() => ++counter)
1520+
1521+
const derived = Atom.make((get) => {
1522+
get(state) // Access state first
1523+
get.refresh(state) // Force refresh of dependency
1524+
return get(state) // Get the refreshed value
1525+
})
1526+
1527+
expect(r.get(derived)).toBe(2)
1528+
expect(counter).toBe(2) // Should have been called twice due to refresh
1529+
})
1530+
1531+
it("custom refresh function", () => {
1532+
const r = Registry.make()
1533+
let refreshCalled = false
1534+
const otherValue = "initial"
1535+
1536+
const otherAtom = Atom.make(otherValue)
1537+
const atom = Atom.readable(
1538+
() => "value",
1539+
(refresh) => {
1540+
refreshCalled = true
1541+
refresh(otherAtom) // Refresh a different atom to avoid recursion
1542+
}
1543+
)
1544+
1545+
r.get(atom)
1546+
expect(refreshCalled).toBe(false)
1547+
1548+
r.refresh(atom)
1549+
expect(refreshCalled).toBe(true)
1550+
})
1551+
1552+
it("isAtom type guard", () => {
1553+
const atom = Atom.make(1)
1554+
const notAtom = { value: 1 }
1555+
1556+
expect(Atom.isAtom(atom)).toBe(true)
1557+
expect(Atom.isAtom(notAtom)).toBe(false)
1558+
expect(Atom.isAtom(null)).toBe(false)
1559+
expect(Atom.isAtom(undefined)).toBe(false)
1560+
})
1561+
1562+
it("isWritable type guard", () => {
1563+
const readable = Atom.readable(() => 1)
1564+
const writable = Atom.make(1)
1565+
1566+
expect(Atom.isWritable(readable)).toBe(false)
1567+
expect(Atom.isWritable(writable)).toBe(true)
1568+
})
1569+
1570+
it("atom properties", () => {
1571+
const atom = Atom.make(1)
1572+
1573+
expect(atom.keepAlive).toBe(false)
1574+
expect(atom.lazy).toBe(true)
1575+
expect(typeof atom.read).toBe("function")
1576+
})
1577+
1578+
it("keepAlive modifier", () => {
1579+
const atom = Atom.make(1)
1580+
const keepAliveAtom = Atom.keepAlive(atom)
1581+
1582+
expect(atom.keepAlive).toBe(false)
1583+
expect(keepAliveAtom.keepAlive).toBe(true)
1584+
expect(atom !== keepAliveAtom).toBe(true) // Should be new instance
1585+
})
1586+
1587+
it("autoDispose modifier", () => {
1588+
const atom = Atom.keepAlive(Atom.make(1))
1589+
const autoDisposeAtom = Atom.autoDispose(atom)
1590+
1591+
expect(atom.keepAlive).toBe(true)
1592+
expect(autoDisposeAtom.keepAlive).toBe(false)
1593+
})
1594+
1595+
it("makeRefreshOnSignal", () => {
1596+
const r = Registry.make()
1597+
const signal = Atom.make(0)
1598+
let computations = 0
1599+
1600+
const atom = Atom.make(() => {
1601+
computations++
1602+
return "value"
1603+
})
1604+
1605+
const refreshing = Atom.makeRefreshOnSignal(signal)(atom)
1606+
1607+
expect(r.get(refreshing)).toBe("value")
1608+
expect(computations).toBe(1)
1609+
1610+
// Trigger signal - should cause refresh
1611+
r.set(signal, 1)
1612+
expect(r.get(refreshing)).toBe("value")
1613+
expect(computations).toBe(2) // Should have recomputed
1614+
})
1615+
1616+
it("Reset symbol", () => {
1617+
const r = Registry.make()
1618+
const atom = Atom.fn((arg: number) => Effect.succeed(arg * 2))
1619+
1620+
r.set(atom, 5)
1621+
const result1 = r.get(atom)
1622+
expect(Result.isSuccess(result1)).toBe(true)
1623+
1624+
r.set(atom, Atom.Reset)
1625+
const result2 = r.get(atom)
1626+
expect(Result.isInitial(result2)).toBe(true)
1627+
})
1628+
1629+
it("Interrupt symbol", () => {
1630+
const r = Registry.make()
1631+
const atom = Atom.fn((arg: number) => Effect.delay(Effect.succeed(arg * 2), "100 millis"))
1632+
r.set(atom, 5)
1633+
const result1 = r.get(atom)
1634+
expect(Result.isWaiting(result1)).toBe(true)
1635+
1636+
r.set(atom, Atom.Interrupt)
1637+
// Should interrupt the running effect
1638+
})
13511639
})
13521640

13531641
interface BuildCounter {

0 commit comments

Comments
 (0)