mirror of
https://github.com/hanzoai/gui.git
synced 2026-10-25 02:31:41 +00:00
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.
327 lines
8.8 KiB
TypeScript
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
|
|
// }
|
|
// }
|