@pyreon/elements 0.24.4 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -12
- package/src/Element/component.tsx +0 -315
- package/src/Element/constants.ts +0 -96
- package/src/Element/index.ts +0 -6
- package/src/Element/types.ts +0 -168
- package/src/Element/utils.ts +0 -15
- package/src/List/component.tsx +0 -105
- package/src/List/index.ts +0 -5
- package/src/Overlay/component.tsx +0 -140
- package/src/Overlay/context.tsx +0 -36
- package/src/Overlay/index.ts +0 -7
- package/src/Overlay/positioning.ts +0 -191
- package/src/Overlay/useOverlay.tsx +0 -461
- package/src/Portal/component.tsx +0 -54
- package/src/Portal/index.ts +0 -5
- package/src/Text/component.tsx +0 -67
- package/src/Text/index.ts +0 -5
- package/src/Text/styled.ts +0 -30
- package/src/Util/component.tsx +0 -43
- package/src/Util/index.ts +0 -5
- package/src/__tests__/Content.test.tsx +0 -123
- package/src/__tests__/Element-slot-reactivity.browser.test.tsx +0 -152
- package/src/__tests__/Element.test.ts +0 -819
- package/src/__tests__/Iterator.test.ts +0 -492
- package/src/__tests__/Iterator.types.test.ts +0 -237
- package/src/__tests__/List.test.ts +0 -199
- package/src/__tests__/Overlay.test.ts +0 -492
- package/src/__tests__/Portal.test.ts +0 -156
- package/src/__tests__/Text.test.ts +0 -274
- package/src/__tests__/Util.test.ts +0 -63
- package/src/__tests__/Wrapper-innerhtml.test.tsx +0 -178
- package/src/__tests__/Wrapper.test.tsx +0 -196
- package/src/__tests__/elements.browser.test.tsx +0 -132
- package/src/__tests__/equalBeforeAfter.test.ts +0 -122
- package/src/__tests__/helpers.test.ts +0 -65
- package/src/__tests__/integration.test.tsx +0 -118
- package/src/__tests__/internElementBundle.test.ts +0 -102
- package/src/__tests__/iterator-function-children.test.tsx +0 -120
- package/src/__tests__/native-markers.test.ts +0 -13
- package/src/__tests__/overlayContext.test.tsx +0 -78
- package/src/__tests__/perf-stress.browser.test.tsx +0 -119
- package/src/__tests__/positioning.test.ts +0 -90
- package/src/__tests__/responsiveProps.test.ts +0 -328
- package/src/__tests__/slot-component-reference.test.tsx +0 -157
- package/src/__tests__/useOverlay.test.ts +0 -1336
- package/src/__tests__/utils.test.ts +0 -69
- package/src/__tests__/wrapper-block-cascade.test.ts +0 -121
- package/src/constants.ts +0 -1
- package/src/env.d.ts +0 -6
- package/src/helpers/Content/component.tsx +0 -75
- package/src/helpers/Content/index.ts +0 -3
- package/src/helpers/Content/styled.ts +0 -105
- package/src/helpers/Content/types.ts +0 -49
- package/src/helpers/Iterator/component.tsx +0 -316
- package/src/helpers/Iterator/index.ts +0 -30
- package/src/helpers/Iterator/types.ts +0 -138
- package/src/helpers/Wrapper/component.tsx +0 -180
- package/src/helpers/Wrapper/constants.ts +0 -10
- package/src/helpers/Wrapper/index.ts +0 -3
- package/src/helpers/Wrapper/styled.ts +0 -64
- package/src/helpers/Wrapper/types.ts +0 -56
- package/src/helpers/Wrapper/utils.ts +0 -7
- package/src/helpers/index.ts +0 -4
- package/src/helpers/internElementBundle.ts +0 -37
- package/src/helpers/isPyreonComponent.ts +0 -46
- package/src/index.ts +0 -42
- package/src/manifest.ts +0 -190
- package/src/tests/manifest-snapshot.test.ts +0 -45
- package/src/types.ts +0 -112
- package/src/utils.ts +0 -5
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for internElementBundle() — the module-scope LRU cache that
|
|
3
|
-
* gives same-shape Element layouts a stable object identity. Locks in the
|
|
4
|
-
* bail conditions (functions, non-string objects) and the LRU semantics
|
|
5
|
-
* so a future refactor can't silently break the styler classCache hit.
|
|
6
|
-
*/
|
|
7
|
-
import { describe, expect, it } from 'vitest'
|
|
8
|
-
import { internElementBundle } from '../helpers/internElementBundle'
|
|
9
|
-
|
|
10
|
-
describe('internElementBundle', () => {
|
|
11
|
-
it('returns the same identity for the same primitive prop tuple', () => {
|
|
12
|
-
const a = internElementBundle({
|
|
13
|
-
block: false,
|
|
14
|
-
direction: 'inline',
|
|
15
|
-
alignX: 'left',
|
|
16
|
-
alignY: 'center',
|
|
17
|
-
equalCols: false,
|
|
18
|
-
extraStyles: undefined,
|
|
19
|
-
})
|
|
20
|
-
const b = internElementBundle({
|
|
21
|
-
block: false,
|
|
22
|
-
direction: 'inline',
|
|
23
|
-
alignX: 'left',
|
|
24
|
-
alignY: 'center',
|
|
25
|
-
equalCols: false,
|
|
26
|
-
extraStyles: undefined,
|
|
27
|
-
})
|
|
28
|
-
expect(a).toBe(b)
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('returns different identity when any primitive value differs', () => {
|
|
32
|
-
const a = internElementBundle({ block: false, direction: 'inline' })
|
|
33
|
-
const b = internElementBundle({ block: true, direction: 'inline' })
|
|
34
|
-
const c = internElementBundle({ block: false, direction: 'rows' })
|
|
35
|
-
expect(a).not.toBe(b)
|
|
36
|
-
expect(a).not.toBe(c)
|
|
37
|
-
expect(b).not.toBe(c)
|
|
38
|
-
})
|
|
39
|
-
|
|
40
|
-
it('handles different bundle shapes independently (parentFix vs childFix)', () => {
|
|
41
|
-
const parent = internElementBundle({
|
|
42
|
-
parentFix: true,
|
|
43
|
-
block: false,
|
|
44
|
-
extraStyles: undefined,
|
|
45
|
-
})
|
|
46
|
-
const child = internElementBundle({
|
|
47
|
-
childFix: true,
|
|
48
|
-
direction: 'inline',
|
|
49
|
-
alignX: 'left',
|
|
50
|
-
alignY: 'center',
|
|
51
|
-
equalCols: false,
|
|
52
|
-
})
|
|
53
|
-
expect(parent).not.toBe(child)
|
|
54
|
-
// Same shape repeats are interned
|
|
55
|
-
const parent2 = internElementBundle({
|
|
56
|
-
parentFix: true,
|
|
57
|
-
block: false,
|
|
58
|
-
extraStyles: undefined,
|
|
59
|
-
})
|
|
60
|
-
expect(parent).toBe(parent2)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('bails (returns the input untouched) when a value is a function', () => {
|
|
64
|
-
const fn = () => 'red'
|
|
65
|
-
const a = internElementBundle({ block: false, extraStyles: fn })
|
|
66
|
-
const b = internElementBundle({ block: false, extraStyles: fn })
|
|
67
|
-
// Both calls bail — neither is cached, so identities differ.
|
|
68
|
-
expect(a).not.toBe(b)
|
|
69
|
-
// The returned object IS the input (untouched).
|
|
70
|
-
expect(a).toEqual({ block: false, extraStyles: fn })
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('bails when a value is a non-null object (CSSResult / nested object)', () => {
|
|
74
|
-
const cssResult = { strings: ['color: red'], values: [] }
|
|
75
|
-
const a = internElementBundle({ block: false, extraStyles: cssResult })
|
|
76
|
-
const b = internElementBundle({ block: false, extraStyles: cssResult })
|
|
77
|
-
expect(a).not.toBe(b)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('treats string extraStyles as cacheable (the common pre-resolved CSS case)', () => {
|
|
81
|
-
const a = internElementBundle({ block: false, extraStyles: 'color: red' })
|
|
82
|
-
const b = internElementBundle({ block: false, extraStyles: 'color: red' })
|
|
83
|
-
expect(a).toBe(b)
|
|
84
|
-
const c = internElementBundle({ block: false, extraStyles: 'color: blue' })
|
|
85
|
-
expect(a).not.toBe(c)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('null and undefined are distinct cache keys (JSON serializes them differently)', () => {
|
|
89
|
-
const aNull = internElementBundle({ extraStyles: null })
|
|
90
|
-
const aUndef = internElementBundle({ extraStyles: undefined })
|
|
91
|
-
expect(aNull).not.toBe(aUndef)
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
it('LRU touch on hit moves entry to most-recent position', () => {
|
|
95
|
-
// Hit the same bundle twice — second call should still return the same
|
|
96
|
-
// identity (LRU touch keeps it alive).
|
|
97
|
-
const key = `lru-touch-${Math.random()}`
|
|
98
|
-
const first = internElementBundle({ direction: key })
|
|
99
|
-
const second = internElementBundle({ direction: key })
|
|
100
|
-
expect(first).toBe(second)
|
|
101
|
-
})
|
|
102
|
-
})
|
|
@@ -1,120 +0,0 @@
|
|
|
1
|
-
/** @jsxImportSource @pyreon/core */
|
|
2
|
-
/**
|
|
3
|
-
* Regression: `<Iterator>{items}</Iterator>` where the Pyreon compiler
|
|
4
|
-
* wrapped `items` in `() => items` (the prop-inlining pass) used to
|
|
5
|
-
* silently misrender — the `Array.isArray(children)` and Fragment-type
|
|
6
|
-
* checks both fell through (a function is neither), and the fallthrough
|
|
7
|
-
* `renderChild(function)` called `render(function, props)` which
|
|
8
|
-
* interpreted the function as a COMPONENT FUNCTION. That accidentally
|
|
9
|
-
* worked at the DOM level (the wrapped function's call returned the array
|
|
10
|
-
* and mountChild rendered it), but the per-item metadata (`first`/`last`/
|
|
11
|
-
* `position`/`index`/`odd`/`even`) was LOST because the iteration loop
|
|
12
|
-
* was never reached.
|
|
13
|
-
*
|
|
14
|
-
* Fix: unwrap eagerly at component-body entry — `typeof children ===
|
|
15
|
-
* 'function' ? children() : children`. Mirrors kinetic's `resolveChildren`
|
|
16
|
-
* pattern (PR #731 + top-level Transition/Stagger parallel fix).
|
|
17
|
-
*
|
|
18
|
-
* Bisect-verified: reverting the `typeof rawChildren === 'function'`
|
|
19
|
-
* unwrap fails this spec — per-item `first`/`last` props arrive as
|
|
20
|
-
* `undefined` because the iteration loop was skipped.
|
|
21
|
-
*/
|
|
22
|
-
import type { VNode, VNodeChild } from '@pyreon/core'
|
|
23
|
-
import { h } from '@pyreon/core'
|
|
24
|
-
import { mount } from '@pyreon/runtime-dom'
|
|
25
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
26
|
-
import Iterator from '../helpers/Iterator/component'
|
|
27
|
-
|
|
28
|
-
let containers: HTMLElement[] = []
|
|
29
|
-
afterEach(() => {
|
|
30
|
-
for (const c of containers) c.remove()
|
|
31
|
-
containers = []
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
describe('<Iterator> — function-wrapped children', () => {
|
|
35
|
-
it('iterates function-wrapped children with per-item metadata via wrapProps', () => {
|
|
36
|
-
let firstFlagSet = false
|
|
37
|
-
let lastFlagSet = false
|
|
38
|
-
const positions: number[] = []
|
|
39
|
-
|
|
40
|
-
// ItemWrapper just renders <li>. The per-item metadata is captured
|
|
41
|
-
// via the `wrapProps` injector — Iterator calls it with the
|
|
42
|
-
// attached metadata `{first, last, position, index, odd, even}`.
|
|
43
|
-
// Counting positions/flags proves the iteration loop fired.
|
|
44
|
-
const ItemWrapper = (props: {
|
|
45
|
-
children?: VNodeChild
|
|
46
|
-
'data-first'?: string
|
|
47
|
-
'data-last'?: string
|
|
48
|
-
'data-position'?: number
|
|
49
|
-
}) => h('li', props as never, props.children as never)
|
|
50
|
-
|
|
51
|
-
const items: VNode[] = [
|
|
52
|
-
h('span', { 'data-id': 'item-a' }, 'A'),
|
|
53
|
-
h('span', { 'data-id': 'item-b' }, 'B'),
|
|
54
|
-
h('span', { 'data-id': 'item-c' }, 'C'),
|
|
55
|
-
]
|
|
56
|
-
|
|
57
|
-
// Compiler-emitted shape: children is `() => items` (the prop-inlining
|
|
58
|
-
// wrap for stable references). Pre-fix: the function was treated as a
|
|
59
|
-
// component → iteration loop skipped → per-item metadata not attached.
|
|
60
|
-
const tree = h(Iterator, {
|
|
61
|
-
wrapComponent: ItemWrapper,
|
|
62
|
-
// wrapProps receives `(itemProps, extendedProps)` where
|
|
63
|
-
// extendedProps carries the per-item metadata. Captured here to
|
|
64
|
-
// prove the iteration loop fired (which the fallthrough wouldn't).
|
|
65
|
-
wrapProps: (_: object, ext: { first: boolean; last: boolean; position: number }) => {
|
|
66
|
-
positions.push(ext.position)
|
|
67
|
-
if (ext.first) firstFlagSet = true
|
|
68
|
-
if (ext.last) lastFlagSet = true
|
|
69
|
-
return {}
|
|
70
|
-
},
|
|
71
|
-
children: (() => items) as unknown as VNodeChild,
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
const container = document.createElement('div')
|
|
75
|
-
document.body.appendChild(container)
|
|
76
|
-
containers.push(container)
|
|
77
|
-
|
|
78
|
-
const dispose = mount(tree as VNode, container)
|
|
79
|
-
|
|
80
|
-
expect(
|
|
81
|
-
positions,
|
|
82
|
-
`wrapProps was called for positions=${JSON.stringify(positions)}; ` +
|
|
83
|
-
`expected [1,2,3] (per-item iteration loop must fire). ` +
|
|
84
|
-
`html=${container.innerHTML.slice(0, 400)}`,
|
|
85
|
-
).toEqual([1, 2, 3])
|
|
86
|
-
expect(firstFlagSet, 'first=true metadata must reach wrapProps').toBe(true)
|
|
87
|
-
expect(lastFlagSet, 'last=true metadata must reach wrapProps').toBe(true)
|
|
88
|
-
|
|
89
|
-
expect(container.querySelector('[data-id="item-a"]')?.tagName).toBe('SPAN')
|
|
90
|
-
expect(container.querySelector('[data-id="item-b"]')?.tagName).toBe('SPAN')
|
|
91
|
-
expect(container.querySelector('[data-id="item-c"]')?.tagName).toBe('SPAN')
|
|
92
|
-
|
|
93
|
-
dispose()
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
it('static-array children control — was always working', () => {
|
|
97
|
-
let renderedItems = 0
|
|
98
|
-
const ItemWrapper = (props: { children?: VNodeChild; position?: number }) => {
|
|
99
|
-
renderedItems++
|
|
100
|
-
return h('li', { 'data-position': props.position }, props.children as never)
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const tree = h(
|
|
104
|
-
Iterator,
|
|
105
|
-
{ wrapComponent: ItemWrapper },
|
|
106
|
-
h('span', { 'data-id': 'static-a' }, 'A'),
|
|
107
|
-
h('span', { 'data-id': 'static-b' }, 'B'),
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
const container = document.createElement('div')
|
|
111
|
-
document.body.appendChild(container)
|
|
112
|
-
containers.push(container)
|
|
113
|
-
const dispose = mount(tree as VNode, container)
|
|
114
|
-
|
|
115
|
-
expect(renderedItems).toBe(2)
|
|
116
|
-
expect(container.querySelector('[data-id="static-a"]')?.tagName).toBe('SPAN')
|
|
117
|
-
|
|
118
|
-
dispose()
|
|
119
|
-
})
|
|
120
|
-
})
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { isNativeCompat } from '@pyreon/core'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import Overlay from '../Overlay/component'
|
|
4
|
-
import OverlayContextProvider from '../Overlay/context'
|
|
5
|
-
|
|
6
|
-
describe('native-compat markers — @pyreon/elements', () => {
|
|
7
|
-
it('Overlay is marked native', () => {
|
|
8
|
-
expect(isNativeCompat(Overlay)).toBe(true)
|
|
9
|
-
})
|
|
10
|
-
it('Overlay context Provider is marked native', () => {
|
|
11
|
-
expect(isNativeCompat(OverlayContextProvider)).toBe(true)
|
|
12
|
-
})
|
|
13
|
-
})
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { popContext } from '@pyreon/core'
|
|
2
|
-
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import OverlayContextProvider, { useOverlayContext } from '../Overlay/context'
|
|
4
|
-
|
|
5
|
-
describe('Overlay context', () => {
|
|
6
|
-
it('useOverlayContext is a function', () => {
|
|
7
|
-
expect(typeof useOverlayContext).toBe('function')
|
|
8
|
-
})
|
|
9
|
-
|
|
10
|
-
it('returns the default context (empty object) when called outside a provider', () => {
|
|
11
|
-
const ctx = useOverlayContext()
|
|
12
|
-
expect(ctx).toEqual({})
|
|
13
|
-
})
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe('OverlayContextProvider component', () => {
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
try {
|
|
19
|
-
popContext()
|
|
20
|
-
} catch {
|
|
21
|
-
// Ignore if no context was pushed
|
|
22
|
-
}
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
it('provides blocked/setBlocked/setUnblocked via context', () => {
|
|
26
|
-
const setBlocked = vi.fn()
|
|
27
|
-
const setUnblocked = vi.fn()
|
|
28
|
-
|
|
29
|
-
OverlayContextProvider({
|
|
30
|
-
blocked: true,
|
|
31
|
-
setBlocked,
|
|
32
|
-
setUnblocked,
|
|
33
|
-
children: 'child',
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const ctx = useOverlayContext()
|
|
37
|
-
expect(ctx.blocked).toBe(true)
|
|
38
|
-
expect(ctx.setBlocked).toBe(setBlocked)
|
|
39
|
-
expect(ctx.setUnblocked).toBe(setUnblocked)
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
it('renders children (returns a value)', () => {
|
|
43
|
-
const result = OverlayContextProvider({
|
|
44
|
-
blocked: false,
|
|
45
|
-
setBlocked: vi.fn(),
|
|
46
|
-
setUnblocked: vi.fn(),
|
|
47
|
-
children: 'Hello overlay',
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
expect(result).toBeDefined()
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('provides blocked as false', () => {
|
|
54
|
-
OverlayContextProvider({
|
|
55
|
-
blocked: false,
|
|
56
|
-
setBlocked: vi.fn(),
|
|
57
|
-
setUnblocked: vi.fn(),
|
|
58
|
-
children: null,
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
const ctx = useOverlayContext()
|
|
62
|
-
expect(ctx.blocked).toBe(false)
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
it('provides blocked as a function when passed as a function', () => {
|
|
66
|
-
const blockedFn = () => true
|
|
67
|
-
|
|
68
|
-
OverlayContextProvider({
|
|
69
|
-
blocked: blockedFn,
|
|
70
|
-
setBlocked: vi.fn(),
|
|
71
|
-
setUnblocked: vi.fn(),
|
|
72
|
-
children: null,
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
const ctx = useOverlayContext()
|
|
76
|
-
expect(ctx.blocked).toBe(blockedFn)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Wall-clock stress benchmark for the Element + Wrapper + Styled stack.
|
|
3
|
-
*
|
|
4
|
-
* Runs in real Chromium. Goal: surface a measurable wall-clock delta that
|
|
5
|
-
* synthetic counter probes (happy-dom + mountChild) miss. Specifically
|
|
6
|
-
* targets the path where Pyreon's 9ms benchmark numbers come from — the
|
|
7
|
-
* mount-pipeline + styler-resolve composition.
|
|
8
|
-
*
|
|
9
|
-
* Each test mounts N components, disposes, and reports median wall-clock
|
|
10
|
-
* across 5 measured iterations after warmup. Variance ≤ 15% on stable runs.
|
|
11
|
-
*/
|
|
12
|
-
import { h, type VNodeChild } from '@pyreon/core'
|
|
13
|
-
import { mount } from '@pyreon/runtime-dom'
|
|
14
|
-
import { flush, mountInBrowser } from '@pyreon/test-utils/browser'
|
|
15
|
-
import { describe, expect, it } from 'vitest'
|
|
16
|
-
import Element from '../Element/component'
|
|
17
|
-
|
|
18
|
-
interface Bench {
|
|
19
|
-
median: number
|
|
20
|
-
min: number
|
|
21
|
-
max: number
|
|
22
|
-
runs: number[]
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
async function benchmark(N: number, mountFn: (root: Element, i: number) => () => void): Promise<Bench> {
|
|
26
|
-
const { container, unmount: cleanup } = mountInBrowser(h('div', { id: 'bench-root' }))
|
|
27
|
-
const root = container.querySelector('#bench-root')!
|
|
28
|
-
|
|
29
|
-
// Warmup — primes the styler sheet cache + GC any dead objects from prior runs.
|
|
30
|
-
for (let w = 0; w < 50; w++) {
|
|
31
|
-
const dispose = mountFn(root, w)
|
|
32
|
-
dispose()
|
|
33
|
-
}
|
|
34
|
-
await flush()
|
|
35
|
-
|
|
36
|
-
const runs: number[] = []
|
|
37
|
-
for (let r = 0; r < 5; r++) {
|
|
38
|
-
const t0 = performance.now()
|
|
39
|
-
const disposers: Array<() => void> = []
|
|
40
|
-
for (let i = 0; i < N; i++) disposers.push(mountFn(root, i))
|
|
41
|
-
for (const d of disposers) d()
|
|
42
|
-
runs.push(performance.now() - t0)
|
|
43
|
-
await flush()
|
|
44
|
-
}
|
|
45
|
-
cleanup()
|
|
46
|
-
|
|
47
|
-
const sorted = [...runs].sort((a, b) => a - b)
|
|
48
|
-
const median = sorted[2] as number
|
|
49
|
-
const min = sorted[0] as number
|
|
50
|
-
const max = sorted[4] as number
|
|
51
|
-
return { median, min, max, runs }
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
describe('Element + stack stress benchmark', () => {
|
|
55
|
-
it('500 bare Element mounts (mount + dispose, batched)', async () => {
|
|
56
|
-
const bench = await benchmark(500, (root, i) => mount(h(Element, null, `item-${i}`), root))
|
|
57
|
-
// oxlint-disable-next-line no-console
|
|
58
|
-
console.log(
|
|
59
|
-
`[stress] 500 bare Element: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
60
|
-
)
|
|
61
|
-
expect(bench.median).toBeLessThan(200)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('500 Element with css prop (exercises extendCss path)', async () => {
|
|
65
|
-
const bench = await benchmark(500, (root, i) =>
|
|
66
|
-
mount(
|
|
67
|
-
h(Element, { css: { color: 'red', padding: 8 } as unknown as Record<string, unknown> }, `item-${i}`),
|
|
68
|
-
root,
|
|
69
|
-
),
|
|
70
|
-
)
|
|
71
|
-
// oxlint-disable-next-line no-console
|
|
72
|
-
console.log(
|
|
73
|
-
`[stress] 500 Element + css: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
74
|
-
)
|
|
75
|
-
expect(bench.median).toBeLessThan(500)
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('depth-10 Element nesting × 50 mounts', async () => {
|
|
79
|
-
const buildDepth = (n: number, label: string): VNodeChild => {
|
|
80
|
-
if (n === 0) return label
|
|
81
|
-
return h(Element, null, buildDepth(n - 1, label))
|
|
82
|
-
}
|
|
83
|
-
const bench = await benchmark(50, (root, i) =>
|
|
84
|
-
mount(buildDepth(10, `leaf-${i}`) as unknown as Parameters<typeof mount>[0], root),
|
|
85
|
-
)
|
|
86
|
-
// oxlint-disable-next-line no-console
|
|
87
|
-
console.log(
|
|
88
|
-
`[stress] 50 depth-10: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
89
|
-
)
|
|
90
|
-
expect(bench.median).toBeLessThan(500)
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
// Larger workload — clearer signal-to-noise. Mount + dispose 5000 elements.
|
|
94
|
-
it('5000 Element mounts — large workload, clearer wall-clock signal', async () => {
|
|
95
|
-
const bench = await benchmark(5000, (root, i) => mount(h(Element, null, `item-${i}`), root))
|
|
96
|
-
// oxlint-disable-next-line no-console
|
|
97
|
-
console.log(
|
|
98
|
-
`[stress] 5000 bare Element: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
99
|
-
)
|
|
100
|
-
expect(bench.median).toBeLessThan(2000)
|
|
101
|
-
})
|
|
102
|
-
|
|
103
|
-
// One-shot single-tree mount: reflects real-app cold-mount cost.
|
|
104
|
-
it('single one-shot mount of a 500-Element tree', async () => {
|
|
105
|
-
const bench = await benchmark(1, (root) => {
|
|
106
|
-
const tree = h(
|
|
107
|
-
'div',
|
|
108
|
-
null,
|
|
109
|
-
...Array.from({ length: 500 }, (_, i) => h(Element, null, `child-${i}`)),
|
|
110
|
-
)
|
|
111
|
-
return mount(tree, root)
|
|
112
|
-
})
|
|
113
|
-
// oxlint-disable-next-line no-console
|
|
114
|
-
console.log(
|
|
115
|
-
`[stress] 500-child tree mount: median=${bench.median.toFixed(2)}ms, runs=[${bench.runs.map((r) => r.toFixed(1)).join(', ')}]`,
|
|
116
|
-
)
|
|
117
|
-
expect(bench.median).toBeLessThan(1000)
|
|
118
|
-
})
|
|
119
|
-
})
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
adjustForAncestor,
|
|
4
|
-
calcDropdownHorizontal,
|
|
5
|
-
calcDropdownVertical,
|
|
6
|
-
calcModalPos,
|
|
7
|
-
} from '../Overlay/positioning'
|
|
8
|
-
|
|
9
|
-
const rect = (top: number, left: number, width: number, height: number): DOMRect => ({
|
|
10
|
-
top,
|
|
11
|
-
left,
|
|
12
|
-
right: left + width,
|
|
13
|
-
bottom: top + height,
|
|
14
|
-
width,
|
|
15
|
-
height,
|
|
16
|
-
x: left,
|
|
17
|
-
y: top,
|
|
18
|
-
toJSON: () => ({ top, left, width, height }),
|
|
19
|
-
})
|
|
20
|
-
|
|
21
|
-
describe('positioning helpers — browser path (happy-dom)', () => {
|
|
22
|
-
it('calcDropdownVertical: fits below → positions under the trigger', () => {
|
|
23
|
-
const trigger = rect(100, 200, 80, 30) // top=100
|
|
24
|
-
const content = rect(0, 0, 60, 40)
|
|
25
|
-
const result = calcDropdownVertical(content, trigger, 'bottom', 'left', 0, 4)
|
|
26
|
-
expect(result.resolvedAlignY).toBe('bottom')
|
|
27
|
-
expect(result.pos.top).toBe(trigger.bottom + 4)
|
|
28
|
-
})
|
|
29
|
-
|
|
30
|
-
it('calcDropdownHorizontal: flips to right when left does not fit', () => {
|
|
31
|
-
const trigger = rect(100, 10, 80, 30) // near left edge
|
|
32
|
-
const content = rect(0, 0, 120, 40) // wider than trigger.left
|
|
33
|
-
const result = calcDropdownHorizontal(content, trigger, 'left', 'top', 0, 0)
|
|
34
|
-
expect(result.resolvedAlignX).toBe('right')
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
it('calcModalPos: centers horizontally and vertically', () => {
|
|
38
|
-
const content = rect(0, 0, 400, 300)
|
|
39
|
-
const pos = calcModalPos(content, 'center', 'center', 0, 0)
|
|
40
|
-
expect(typeof pos.left).toBe('number')
|
|
41
|
-
expect(typeof pos.top).toBe('number')
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
it('adjustForAncestor: subtracts ancestor offset from absolute positions', () => {
|
|
45
|
-
const adjusted = adjustForAncestor({ top: 100, left: 200 }, { top: 30, left: 50 })
|
|
46
|
-
expect(adjusted.top).toBe(70)
|
|
47
|
-
expect(adjusted.left).toBe(150)
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('adjustForAncestor: returns input unchanged when ancestor is origin', () => {
|
|
51
|
-
const input = { top: 10, left: 20 }
|
|
52
|
-
expect(adjustForAncestor(input, { top: 0, left: 0 })).toBe(input)
|
|
53
|
-
})
|
|
54
|
-
})
|
|
55
|
-
|
|
56
|
-
// SSR-fallback path: the positioning helpers are only reachable via mounted
|
|
57
|
-
// event handlers in production, so `typeof window === 'undefined'` never
|
|
58
|
-
// fires in browser tests. We force the fallback by stubbing `globalThis.window`
|
|
59
|
-
// to `undefined` for the duration of a single test. happy-dom resets on
|
|
60
|
-
// teardown; we restore the original window afterwards.
|
|
61
|
-
describe('positioning helpers — SSR fallback (window undefined)', () => {
|
|
62
|
-
const realWindow = globalThis.window
|
|
63
|
-
const realWindowDesc = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
|
64
|
-
|
|
65
|
-
afterEach(() => {
|
|
66
|
-
if (realWindowDesc) Object.defineProperty(globalThis, 'window', realWindowDesc)
|
|
67
|
-
else (globalThis as { window?: unknown }).window = realWindow
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
it('calcDropdownVertical returns alignment-preserving empty fallback', () => {
|
|
71
|
-
Object.defineProperty(globalThis, 'window', { value: undefined, configurable: true, writable: true })
|
|
72
|
-
const result = calcDropdownVertical(rect(0, 0, 60, 40), rect(100, 200, 80, 30), 'bottom', 'left', 0, 0)
|
|
73
|
-
expect(result.pos).toEqual({})
|
|
74
|
-
expect(result.resolvedAlignX).toBe('left')
|
|
75
|
-
expect(result.resolvedAlignY).toBe('bottom')
|
|
76
|
-
})
|
|
77
|
-
|
|
78
|
-
it('calcDropdownHorizontal returns alignment-preserving empty fallback', () => {
|
|
79
|
-
Object.defineProperty(globalThis, 'window', { value: undefined, configurable: true, writable: true })
|
|
80
|
-
const result = calcDropdownHorizontal(rect(0, 0, 60, 40), rect(100, 200, 80, 30), 'right', 'top', 0, 0)
|
|
81
|
-
expect(result.pos).toEqual({})
|
|
82
|
-
expect(result.resolvedAlignX).toBe('right')
|
|
83
|
-
expect(result.resolvedAlignY).toBe('top')
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('calcModalPos returns empty object', () => {
|
|
87
|
-
Object.defineProperty(globalThis, 'window', { value: undefined, configurable: true, writable: true })
|
|
88
|
-
expect(calcModalPos(rect(0, 0, 400, 300), 'center', 'center', 0, 0)).toEqual({})
|
|
89
|
-
})
|
|
90
|
-
})
|