Files
zeekay 6d0dd44bfd chore: flatten pkgs/packages/* → pkgs/* (one canonical pkgs dir)
ONE WAY ONLY: 'pkgs' is the canonical name for any directory holding
packages. The redundant 'pkgs/packages/' sub-level is dropped — every
package now lives at pkgs/<name>/ (with categorical sub-groups
pkgs/compiler/, pkgs/core/, pkgs/ui/ unchanged since they are
sub-systems, not just 'packages').

- mv pkgs/packages/* → pkgs/* (24 packages)
- rm pkgs/packages/hanzogui-dev-config (orphan: only had dist/ artifacts,
  no source; gui-dev-config is the canonical @hanzogui/dev-config)
- package.json workspaces: drop pkgs/packages glob, add ./pkgs/*
- tsconfig.json: rewrite paths + 15 references entries
- apps/{demos,kitchen-sink,gui.hanzo.ai}/tsconfig.json: reference paths
- .github/workflows/publish-gui.yml: directory walk list
- pkgs/{get,recipes}/package.json: repository.directory

bun.lock will regenerate on next install.
2026-05-27 17:33:42 -07:00

327 lines
8.8 KiB
TypeScript

/**
* @jest-environment jsdom
*/
import type { RenderResult } from '@testing-library/react'
import { act, cleanup, fireEvent, render } from '@testing-library/react'
import { StrictMode } from 'react'
import { afterEach, describe, expect, it } from 'vitest'
import {
Store,
createStore,
useGlobalStore,
useGlobalStoreSelector,
useStore,
} from '../src'
Error.stackTraceLimit = Number.POSITIVE_INFINITY
// configureUseStore({
// logLevel: 'debug',
// })
const sleep = (x = 100) => new Promise((res) => setTimeout(res, x))
type Todo = { text: string; done: boolean }
class TodoList extends Store<{
id: number
}> {
items: Todo[] = [{ text: 'hi', done: false }]
alt = ''
get itemsDiff() {
return this.items.map((x, i) => i)
}
get lastItem() {
return this.items[this.items.length - 1]
}
changeAlt() {
this.alt = `${Math.random()}`
}
add() {
this.items = [...this.items, { text: `item-${this.items.length}`, done: false }]
}
async asyncAdd() {
await sleep(20)
this.add()
}
}
class Store2 extends Store<{ id: number }> {
x = 0
add() {
this.x = 1
}
}
const last = (x: any[]) => x[x.length - 1]
async function testSimpleStore(id: number) {
const { getAllByTitle } = render(
<StrictMode>
<SimpleStoreTest id={id} />
</StrictMode>
)
const getCurrentByTitle = (name: string) => last(getAllByTitle(name))!
const findX = () => getCurrentByTitle('x').innerHTML
expect(findX()).toBe('hi')
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
expect(findX()).toBe('item-1')
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
expect(findX()).toBe('item-2')
act(() => {
fireEvent.click(getCurrentByTitle('addAsync'))
})
await act(async () => {
await sleep(100)
expect(findX()).toBe('item-3')
})
}
// be sure ids are not same across tests...
type RR = RenderResult<typeof import('@testing-library/dom/types/queries')>
const getLastByTitle = (rr: RR, name: string) => last(rr.getAllByTitle(name))!
const findTitle = (rr: RR, title: string) => getLastByTitle(rr, title).innerHTML
describe('basic tests', () => {
afterEach(cleanup)
it('works as singleton creating a global store', () => {
class Store3 extends Store<{ id: number }> {
y = 0
mount() {
this.y = this.props.id + 10
}
get z() {
return this.y - 5
}
add() {
this.y++
}
}
const store3 = createStore(Store3, { id: 1 })
expect(store3.y).toEqual(11)
expect(store3.z).toEqual(6)
function SingletonStore() {
const store = useGlobalStore(store3)
return (
<>
<div title="z">{store.z}</div>
<button title="add" onClick={() => store.add()}></button>
</>
)
}
const r1 = render(<SingletonStore />)
expect(findTitle(r1, 'z')).toBe('6')
act(() => {
fireEvent.click(getLastByTitle(r1, 'add'))
})
expect(findTitle(r1, 'z')).toBe('7')
function SingletonStoreSelector() {
const store = useGlobalStore(store3)
const storeZ = useGlobalStoreSelector(store3, (x) => x.z)
return (
<>
<div title="z">{storeZ}</div>
<button title="add" onClick={() => store.add()}></button>
</>
)
}
const r2 = render(<SingletonStoreSelector />)
expect(findTitle(r2, 'z')).toBe('7')
act(() => {
fireEvent.click(getLastByTitle(r2, 'add'))
})
expect(findTitle(r2, 'z')).toBe('8')
})
// SKIPPING - async seems to fail now but app is working alright
// may be an issue in testing library, though doubtul since all the rest works
// if i add logging in useStore, it works all the way up to getSnapshot,
// you can see it return the new value there so it seems right, but for some
// reason react doesn't re-render the last time, even though it was told to/snapshot changed
it('creates a simple store and action works', async () => {
await testSimpleStore(0)
})
it('creates a second store under diff namespace both work', async () => {
await testSimpleStore(1)
await testSimpleStore(2)
})
it('creates two stores under diff namespace, state is different', async () => {
function MultiStoreUseTest() {
const store = useStore(Store2, { id: 0 })
const store2 = useStore(Store2, { id: 1 })
return (
<>
<div title="x">{store.x}</div>
<div title="x2">{store2.x}</div>
<button
title="add"
onClick={() => {
store.add()
}}
></button>
</>
)
}
const { getAllByTitle } = render(
<StrictMode>
<MultiStoreUseTest />
</StrictMode>
)
const getCurrentByTitle = (name: string) => last(getAllByTitle(name))!
expect(getCurrentByTitle('x').innerHTML).toBe('0')
await act(async () => {
fireEvent.click(getCurrentByTitle('add'))
})
expect(getCurrentByTitle('x').innerHTML).toBe('1')
expect(getCurrentByTitle('x2').innerHTML).toBe('0')
})
it('updates a component in a different tree', async () => {
const { getAllByTitle } = render(
<StrictMode>
<SimpleStoreTest id={4} />
<SimpleStoreTest2 id={4} />
</StrictMode>
)
const getCurrentByTitle = (name: string) => last(getAllByTitle(name))!
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
expect(getCurrentByTitle('x').innerHTML).toBe('item-1')
expect(getCurrentByTitle('x2').innerHTML).toBe('item-1')
})
it('properly updates get values', () => {
const { getAllByTitle } = render(
<StrictMode>
<SimpleStoreTest id={3} />
</StrictMode>
)
const findY = () => getAllByTitle('y')[0].innerHTML
expect(findY()).toBe('0')
act(() => {
fireEvent.click(getAllByTitle('add')[0])
})
expect(findY()).toBe('1')
})
it('only re-renders tracked properties', async () => {
let renderCount = 0
function SimpleStoreTestUsedProperties(props: { id: number }) {
const store = useStore(TodoList, props)
renderCount++
// console.log('render')
return (
<>
<button title="add" onClick={() => store.add()}></button>
<button title="changeAlt" onClick={() => store.changeAlt()}></button>
<div title="alt">{store.alt}</div>
</>
)
}
const { getAllByTitle } = render(<SimpleStoreTestUsedProperties id={5} />)
const getCurrentByTitle = (name: string) => last(getAllByTitle(name))!
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
act(() => {
fireEvent.click(getCurrentByTitle('changeAlt'))
})
expect(renderCount).toEqual(2)
})
it('only re-renders tracked properties (selectors + singleton)', async () => {
let renderCount = 0
const todoStore = createStore(TodoList, { id: 1001 })
function Component() {
// should change twice, first undefined, second not
const thirdItem = useGlobalStoreSelector(todoStore, (x) => x.items[2])
renderCount++
return <button title="add" onClick={() => todoStore.add()}></button>
}
const { getAllByTitle } = render(<Component />)
const getCurrentByTitle = (name: string) => last(getAllByTitle(name))!
for (let i = 0; i < 10; i++) {
act(() => {
fireEvent.click(getCurrentByTitle('add'))
})
}
// Update: fixed this, but leaving note because we may revent useMutableSource at some point
// fixed by not doing setState(prev => {}) but actually avoiding calling setState
// NOTE: its 3 because react does one extra render to be sure (for some reason)
// see: https://reactjs.org/docs/hooks-reference.html#bailing-out-of-a-state-update
expect(renderCount).toBeLessThanOrEqual(3)
})
})
function SimpleStoreTest(props: { id: number }) {
const store = useStore(TodoList, props)
return (
<>
<div title="x">{store.lastItem.text}</div>
<div title="y">{store.itemsDiff[store.itemsDiff.length - 1]}</div>
<button
title="add"
onClick={() => {
act(() => {
store.add()
})
}}
></button>
<button
title="addAsync"
onClick={() => {
act(() => {
store.asyncAdd()
})
}}
></button>
</>
)
}
function SimpleStoreTest2(props: { id: number }) {
const store = useStore(TodoList, props)
return <div title="x2">{store.lastItem.text}</div>
}
// // TODO testing using in combination
// class CustomTodoList extends Store<{ id: number }> {
// todoList = get(TodoList, this.props.id)
// get items() {
// return this.todoList.items
// }
// }