@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.
Files changed (70) hide show
  1. package/package.json +10 -12
  2. package/src/Element/component.tsx +0 -315
  3. package/src/Element/constants.ts +0 -96
  4. package/src/Element/index.ts +0 -6
  5. package/src/Element/types.ts +0 -168
  6. package/src/Element/utils.ts +0 -15
  7. package/src/List/component.tsx +0 -105
  8. package/src/List/index.ts +0 -5
  9. package/src/Overlay/component.tsx +0 -140
  10. package/src/Overlay/context.tsx +0 -36
  11. package/src/Overlay/index.ts +0 -7
  12. package/src/Overlay/positioning.ts +0 -191
  13. package/src/Overlay/useOverlay.tsx +0 -461
  14. package/src/Portal/component.tsx +0 -54
  15. package/src/Portal/index.ts +0 -5
  16. package/src/Text/component.tsx +0 -67
  17. package/src/Text/index.ts +0 -5
  18. package/src/Text/styled.ts +0 -30
  19. package/src/Util/component.tsx +0 -43
  20. package/src/Util/index.ts +0 -5
  21. package/src/__tests__/Content.test.tsx +0 -123
  22. package/src/__tests__/Element-slot-reactivity.browser.test.tsx +0 -152
  23. package/src/__tests__/Element.test.ts +0 -819
  24. package/src/__tests__/Iterator.test.ts +0 -492
  25. package/src/__tests__/Iterator.types.test.ts +0 -237
  26. package/src/__tests__/List.test.ts +0 -199
  27. package/src/__tests__/Overlay.test.ts +0 -492
  28. package/src/__tests__/Portal.test.ts +0 -156
  29. package/src/__tests__/Text.test.ts +0 -274
  30. package/src/__tests__/Util.test.ts +0 -63
  31. package/src/__tests__/Wrapper-innerhtml.test.tsx +0 -178
  32. package/src/__tests__/Wrapper.test.tsx +0 -196
  33. package/src/__tests__/elements.browser.test.tsx +0 -132
  34. package/src/__tests__/equalBeforeAfter.test.ts +0 -122
  35. package/src/__tests__/helpers.test.ts +0 -65
  36. package/src/__tests__/integration.test.tsx +0 -118
  37. package/src/__tests__/internElementBundle.test.ts +0 -102
  38. package/src/__tests__/iterator-function-children.test.tsx +0 -120
  39. package/src/__tests__/native-markers.test.ts +0 -13
  40. package/src/__tests__/overlayContext.test.tsx +0 -78
  41. package/src/__tests__/perf-stress.browser.test.tsx +0 -119
  42. package/src/__tests__/positioning.test.ts +0 -90
  43. package/src/__tests__/responsiveProps.test.ts +0 -328
  44. package/src/__tests__/slot-component-reference.test.tsx +0 -157
  45. package/src/__tests__/useOverlay.test.ts +0 -1336
  46. package/src/__tests__/utils.test.ts +0 -69
  47. package/src/__tests__/wrapper-block-cascade.test.ts +0 -121
  48. package/src/constants.ts +0 -1
  49. package/src/env.d.ts +0 -6
  50. package/src/helpers/Content/component.tsx +0 -75
  51. package/src/helpers/Content/index.ts +0 -3
  52. package/src/helpers/Content/styled.ts +0 -105
  53. package/src/helpers/Content/types.ts +0 -49
  54. package/src/helpers/Iterator/component.tsx +0 -316
  55. package/src/helpers/Iterator/index.ts +0 -30
  56. package/src/helpers/Iterator/types.ts +0 -138
  57. package/src/helpers/Wrapper/component.tsx +0 -180
  58. package/src/helpers/Wrapper/constants.ts +0 -10
  59. package/src/helpers/Wrapper/index.ts +0 -3
  60. package/src/helpers/Wrapper/styled.ts +0 -64
  61. package/src/helpers/Wrapper/types.ts +0 -56
  62. package/src/helpers/Wrapper/utils.ts +0 -7
  63. package/src/helpers/index.ts +0 -4
  64. package/src/helpers/internElementBundle.ts +0 -37
  65. package/src/helpers/isPyreonComponent.ts +0 -46
  66. package/src/index.ts +0 -42
  67. package/src/manifest.ts +0 -190
  68. package/src/tests/manifest-snapshot.test.ts +0 -45
  69. package/src/types.ts +0 -112
  70. 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
- })