@octane-xplat/ui 0.7.2 → 0.9.0
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/README.md +4 -3
- package/dist/native/Button.tsrx.js +1 -1
- package/dist/native/CommandPalette.tsrx.js +7 -1
- package/dist/native/Grid.tsrx.js +31 -2
- package/dist/native/KeyboardAvoiding.mobile.tsrx.js +2 -2
- package/dist/native/Markdown.tsrx.js +233 -0
- package/dist/native/Overlay.tsrx.js +15 -26
- package/dist/native/Pagination.tsrx.js +15 -12
- package/dist/native/PinInput.tsrx.js +25 -7
- package/dist/native/Popover.tsrx.js +33 -23
- package/dist/native/Pressable.tsrx.js +4 -1
- package/dist/native/Screen.tsrx.js +1 -1
- package/dist/native/SearchInput.tsrx.js +6 -3
- package/dist/native/Sheet.tsrx.js +23 -38
- package/dist/native/Slider.tsrx.js +23 -14
- package/dist/native/Text.tsrx.js +16 -0
- package/dist/native/TextArea.tsrx.js +6 -3
- package/dist/native/TextInput.tsrx.js +6 -3
- package/dist/native/VirtualList.tsrx.js +305 -121
- package/dist/native/WebView.tsrx.js +17 -2
- package/dist/native/_virtual/_@oxc-project_runtime@0.150.0/helpers/esm/decorate.js +9 -0
- package/dist/native/escape-props.js +2 -0
- package/dist/native/grid-auto-placement.js +83 -0
- package/dist/native/grid-placement.js +20 -0
- package/dist/native/index.shared.js +7 -5
- package/dist/native/keyboard-inset.mobile.js +5 -5
- package/dist/native/navigation-request.js +26 -0
- package/dist/native/octane-client-build.json +1 -1
- package/dist/native/overlay-lifetime.mobile.js +65 -0
- package/dist/native/pan.tsrx.js +16 -2
- package/dist/native/route-table.js +90 -10
- package/dist/native/route.js +26 -16
- package/dist/native/shared.js +7 -5
- package/dist/native/sheet-service.js +2 -2
- package/dist/native/svg.mobile.js +1 -1
- package/dist/native/{tap-to-blur.mobile.js → tap-to-blur.ios.js} +20 -32
- package/dist/native/text-focus.js +10 -0
- package/dist/native/text-write.js +18 -11
- package/dist/native/toast-anchor.tsrx.js +1 -1
- package/dist/native/ui.js +7 -6
- package/dist/native/useMeasure.tsrx.js +17 -18
- package/dist/native/vendor/ui-svg/index.android.js +2 -2
- package/dist/native/vendor/ui-svg/index.ios.js +2 -2
- package/dist/native/vendor/ui-svg/{index.common.js → src/index.common.js} +1 -1
- package/dist/native/virtual-list-cell-pool.js +63 -0
- package/dist/web/Button.web.tsrx.js +1 -1
- package/dist/web/KeyboardAvoiding.web.tsrx.js +78 -0
- package/dist/web/Markdown.tsrx.js +98 -0
- package/dist/web/Overlay.web.tsrx.js +81 -32
- package/dist/web/PinInput.web.tsrx.js +24 -10
- package/dist/web/Pressable.web.tsrx.js +169 -53
- package/dist/web/SearchInput.web.tsrx.js +2 -2
- package/dist/web/Sheet.web.tsrx.js +20 -7
- package/dist/web/TextArea.web.tsrx.js +22 -17
- package/dist/web/TextInput.web.tsrx.js +2 -2
- package/dist/web/VirtualList.web.tsrx.js +220 -109
- package/dist/web/WebView.web.tsrx.js +3 -3
- package/dist/web/{index.shared.js → index.shared.web.js} +7 -5
- package/dist/web/modal-focus.web.js +95 -0
- package/dist/web/navigation-request.js +26 -0
- package/dist/web/octane-client-build.json +1 -1
- package/dist/web/pan.web.tsrx.js +27 -10
- package/dist/web/route-table.js +90 -10
- package/dist/web/route.web.js +88 -22
- package/dist/web/sheet-service.web.js +8 -0
- package/dist/web/theme/colorScheme.web.js +31 -4
- package/dist/web/ui.js +8 -6
- package/dist/web/virtual-list-cell-pool.js +63 -0
- package/package.json +21 -19
- package/platforms/android/native-api-usage.json +7 -7
- package/src/CommandPalette.tsrx +8 -1
- package/src/Grid.tsrx +32 -1
- package/src/Icon.macos.tsrx +1 -0
- package/src/KeyboardAvoiding.macos.tsrx +7 -1
- package/src/KeyboardAvoiding.mobile.tsrx +2 -2
- package/src/KeyboardAvoiding.tsrx +15 -0
- package/src/KeyboardAvoiding.web.tsrx +15 -0
- package/src/Markdown.tsrx +121 -0
- package/src/Meter.tsrx +1 -0
- package/src/Overlay.tsrx +15 -30
- package/src/Overlay.web.tsrx +12 -3
- package/src/Pagination.tsrx +6 -4
- package/src/PinInput.macos.tsrx +20 -6
- package/src/PinInput.mobile.test.ts +107 -0
- package/src/PinInput.tsrx +20 -6
- package/src/PinInput.web.test.tsrx +42 -0
- package/src/PinInput.web.tsrx +20 -6
- package/src/Popover.tsrx +36 -22
- package/src/Pressable.tsrx +1 -1
- package/src/Pressable.web.test.tsrx +36 -0
- package/src/Pressable.web.tsrx +37 -6
- package/src/RouteHost.macos.tsrx +1 -0
- package/src/Screen.tsrx +1 -1
- package/src/SearchInput.tsrx +4 -3
- package/src/SearchInput.web.tsrx +1 -1
- package/src/Sheet.tsrx +21 -42
- package/src/Sheet.web.tsrx +9 -1
- package/src/Slider.tsrx +13 -11
- package/src/Text.tsrx +36 -0
- package/src/TextArea.tsrx +6 -5
- package/src/TextArea.web.test.tsrx +39 -0
- package/src/TextArea.web.tsrx +7 -5
- package/src/TextInput.tsrx +6 -6
- package/src/TextInput.web.tsrx +1 -1
- package/src/VirtualList.macos.tsrx +93 -6
- package/src/VirtualList.mobile.test.tsrx +232 -0
- package/src/VirtualList.tsrx +285 -81
- package/src/VirtualList.web.tsrx +131 -25
- package/src/WebView.tsrx +20 -10
- package/src/WebView.web.tsrx +3 -0
- package/src/android/BottomNavigationView.android.tsrx +1 -0
- package/src/android/MaterialDialog.android.tsrx +1 -0
- package/src/anim.macos.tsrx +1 -0
- package/src/apply-native-modifiers.native.ts +4 -1
- package/src/child-array.ts +12 -7
- package/src/colorScheme.macos.ts +1 -0
- package/src/controls.macos.tsrx +4 -1
- package/src/data-driver.mobile.test.ts +120 -0
- package/src/data-lifecycle.mobile.test.ts +251 -0
- package/src/escape-props.ts +13 -1
- package/src/grid-auto-placement.ts +163 -0
- package/src/grid-placement.ts +29 -0
- package/src/hosted-unsupported.macos.tsrx +1 -0
- package/src/index.macos.ts +23 -3
- package/src/index.mobile.ts +0 -1
- package/src/index.shared.ts +22 -1
- package/src/index.shared.web.ts +291 -0
- package/src/index.web.ts +1 -1
- package/src/input-readiness.web.test.tsrx +82 -0
- package/src/ios/UITabBar.ios.tsrx +1 -0
- package/src/keyboard-inset.mobile.test.ts +62 -0
- package/src/keyboard-inset.mobile.ts +63 -43
- package/src/layout-shells.macos.tsrx +1 -5
- package/src/macos-extras.macos.tsrx +2 -0
- package/src/modal-focus.web.test.tsrx +63 -0
- package/src/modal-focus.web.ts +162 -0
- package/src/navigation-request.ts +37 -0
- package/src/overlay-lifecycle.mobile.test.ts +316 -0
- package/src/overlay-lifetime.mobile.ts +107 -0
- package/src/pan.mobile.test.tsrx +116 -0
- package/src/pan.tsrx +20 -2
- package/src/pan.web.tsrx +35 -7
- package/src/platformProps.ts +1 -0
- package/src/popover.mobile.test.ts +9 -1
- package/src/pressable-accessibility.mobile.test.ts +30 -0
- package/src/props.ts +163 -3
- package/src/refresh.ts +6 -1
- package/src/refresh.web.test.ts +77 -77
- package/src/route-android.test.ts +162 -0
- package/src/route-history.web.test.ts +80 -0
- package/src/route-navigation.web.test.ts +173 -0
- package/src/route-table.test.ts +188 -9
- package/src/route-table.ts +138 -10
- package/src/route.macos.ts +56 -13
- package/src/route.ts +54 -24
- package/src/route.web.ts +141 -24
- package/src/sheet-detents.ts +5 -3
- package/src/sheet-service.macos.ts +5 -1
- package/src/sheet-service.ts +1 -1
- package/src/sheet-service.web.ts +8 -0
- package/src/sheet-snap.test.ts +23 -23
- package/src/sheet-snap.ts +6 -1
- package/src/svg.mobile.ts +9 -3
- package/src/tap-to-blur.ios.ts +106 -0
- package/src/tap-to-blur.mobile.ts +21 -95
- package/src/text-focus.ts +12 -0
- package/src/text-input.mobile.test.ts +89 -0
- package/src/text-write.ts +21 -11
- package/src/theme/colorScheme.linux.ts +2 -4
- package/src/theme/colorScheme.web.ts +51 -4
- package/src/theme/theme-scheme.macos.ts +8 -2
- package/src/theme/tokens.css +1740 -1718
- package/src/translate.macos.ts +4 -1
- package/src/useMeasure.tsrx +20 -21
- package/src/virtual-list-cell-pool.test.ts +53 -0
- package/src/virtual-list-cell-pool.ts +81 -0
- package/src/virtual-list-layout.test.ts +11 -8
- package/src/virtual-list-layout.ts +4 -8
- package/src/windows.linux.ts +7 -2
- package/src/windows.macos.ts +1 -0
- package/types/generated/.tsrx-typegen-manifest.json +16 -3
- package/types/generated/Grid.d.ts +0 -1
- package/types/generated/KeyboardAvoiding.d.ts +3 -0
- package/types/generated/KeyboardAvoiding.web.d.ts +2 -0
- package/types/generated/Markdown.d.ts +59 -0
- package/types/generated/TextArea.d.ts +2 -2
- package/types/generated/TextArea.web.d.ts +1 -1
- package/types/generated/TextInput.d.ts +2 -3
- package/types/generated/VirtualList.d.ts +3 -3
- package/types/generated/escape-props.d.ts +4 -0
- package/types/generated/grid-auto-placement.d.ts +13 -0
- package/types/generated/grid-placement.d.ts +5 -0
- package/types/generated/index.mobile.d.ts +0 -1
- package/types/generated/index.shared.d.ts +6 -3
- package/types/generated/index.shared.web.d.ts +114 -0
- package/types/generated/index.web.d.ts +1 -1
- package/types/generated/modal-focus.web.d.ts +3 -0
- package/types/generated/navigation-request.d.ts +10 -0
- package/types/generated/overlay-lifetime.mobile.d.ts +10 -0
- package/types/generated/props.d.ts +111 -3
- package/types/generated/route-table.d.ts +14 -3
- package/types/generated/svg.mobile.d.ts +2 -2
- package/types/generated/tap-to-blur.ios.d.ts +13 -0
- package/types/generated/tap-to-blur.mobile.d.ts +1 -15
- package/types/generated/text-focus.d.ts +3 -0
- package/types/generated/text-write.d.ts +3 -1
- package/types/generated/theme/colorScheme.web.d.ts +5 -0
- package/types/generated/virtual-list-cell-pool.d.ts +18 -0
- package/types/index.macos.d.ts +15 -3
- /package/src/vendor/ui-svg/{index.android.ts → src/index.android.ts} +0 -0
- /package/src/vendor/ui-svg/{index.common.ts → src/index.common.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ios.ts → src/index.ios.ts} +0 -0
- /package/src/vendor/ui-svg/{index.ts → src/index.ts} +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/SVGKit.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/android.d.ts +0 -0
- /package/src/vendor/ui-svg/{typings → src/typings}/ios.d.ts +0 -0
- /package/types/generated/vendor/ui-svg/{index.common.d.ts → src/index.common.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.d.ts → src/index.d.ts} +0 -0
- /package/types/generated/vendor/ui-svg/{index.ios.d.ts → src/index.ios.d.ts} +0 -0
package/src/refresh.web.test.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
-
import { createPullToRefresh } from './refresh.web'
|
|
4
|
-
import { REFRESH_HEADER_HEIGHT } from './refresh-metrics'
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { createPullToRefresh } from './refresh.web'
|
|
4
|
+
import { REFRESH_HEADER_HEIGHT } from './refresh-metrics'
|
|
5
5
|
|
|
6
6
|
// Unit-level check of the web pull-to-refresh state machine: synthetic
|
|
7
7
|
// pointer events drive the controller, assertions read the composed
|
|
@@ -10,102 +10,102 @@ import { REFRESH_HEADER_HEIGHT } from './refresh-metrics';
|
|
|
10
10
|
// handlers read.
|
|
11
11
|
|
|
12
12
|
function host() {
|
|
13
|
-
const wrapper = document.createElement('div')
|
|
14
|
-
const scroller = document.createElement('div')
|
|
15
|
-
const indicator = document.createElement('div')
|
|
16
|
-
wrapper.appendChild(indicator)
|
|
17
|
-
wrapper.appendChild(scroller)
|
|
18
|
-
document.body.appendChild(wrapper)
|
|
19
|
-
return { wrapper, scroller, indicator }
|
|
13
|
+
const wrapper = document.createElement('div')
|
|
14
|
+
const scroller = document.createElement('div')
|
|
15
|
+
const indicator = document.createElement('div')
|
|
16
|
+
wrapper.appendChild(indicator)
|
|
17
|
+
wrapper.appendChild(scroller)
|
|
18
|
+
document.body.appendChild(wrapper)
|
|
19
|
+
return { wrapper, scroller, indicator }
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function pointer(type: string, y: number) {
|
|
23
|
-
const e = new Event(type, { cancelable: true, bubbles: true })
|
|
24
|
-
Object.assign(e, { pointerType: 'mouse', clientY: y, button: 0 })
|
|
25
|
-
return e
|
|
23
|
+
const e = new Event(type, { cancelable: true, bubbles: true })
|
|
24
|
+
Object.assign(e, { pointerType: 'mouse', clientY: y, button: 0 })
|
|
25
|
+
return e
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const ty = (el: HTMLElement) => el.style.transform
|
|
28
|
+
const ty = (el: HTMLElement) => el.style.transform
|
|
29
29
|
|
|
30
30
|
describe('createPullToRefresh (web)', () => {
|
|
31
31
|
beforeEach(() => {
|
|
32
|
-
vi.useFakeTimers()
|
|
33
|
-
})
|
|
32
|
+
vi.useFakeTimers()
|
|
33
|
+
})
|
|
34
34
|
|
|
35
35
|
afterEach(() => {
|
|
36
|
-
vi.useRealTimers()
|
|
37
|
-
document.body.innerHTML = ''
|
|
38
|
-
})
|
|
36
|
+
vi.useRealTimers()
|
|
37
|
+
document.body.innerHTML = ''
|
|
38
|
+
})
|
|
39
39
|
|
|
40
40
|
it('tracks a sub-threshold drag and resets on release', () => {
|
|
41
|
-
const { scroller, indicator } = host()
|
|
42
|
-
const cfg = { current: { onRefresh: vi.fn() } }
|
|
43
|
-
const ctl = createPullToRefresh({ scroller, indicator, cfg })
|
|
41
|
+
const { scroller, indicator } = host()
|
|
42
|
+
const cfg = { current: { onRefresh: vi.fn() } }
|
|
43
|
+
const ctl = createPullToRefresh({ scroller, indicator, cfg })
|
|
44
44
|
|
|
45
|
-
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
46
|
-
scroller.dispatchEvent(pointer('pointermove', 180))
|
|
45
|
+
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
46
|
+
scroller.dispatchEvent(pointer('pointermove', 180))
|
|
47
47
|
// damped: 80 * 0.5 = 40
|
|
48
|
-
expect(ty(scroller)).toContain('translateY(40px)')
|
|
49
|
-
expect(ty(indicator)).toContain(`translateY(${40 - REFRESH_HEADER_HEIGHT}px)`)
|
|
48
|
+
expect(ty(scroller)).toContain('translateY(40px)')
|
|
49
|
+
expect(ty(indicator)).toContain(`translateY(${40 - REFRESH_HEADER_HEIGHT}px)`)
|
|
50
50
|
|
|
51
|
-
scroller.dispatchEvent(pointer('pointerup', 180))
|
|
52
|
-
expect(cfg.current.onRefresh).not.toHaveBeenCalled()
|
|
53
|
-
expect(ty(scroller)).toContain('translateY(0px)')
|
|
54
|
-
ctl.detach()
|
|
55
|
-
})
|
|
51
|
+
scroller.dispatchEvent(pointer('pointerup', 180))
|
|
52
|
+
expect(cfg.current.onRefresh).not.toHaveBeenCalled()
|
|
53
|
+
expect(ty(scroller)).toContain('translateY(0px)')
|
|
54
|
+
ctl.detach()
|
|
55
|
+
})
|
|
56
56
|
|
|
57
57
|
it('fires onRefresh and docks past the threshold; refreshless release collapses after grace', () => {
|
|
58
|
-
const { scroller, indicator } = host()
|
|
59
|
-
const cfg = { current: { onRefresh: vi.fn(), refreshing: false } }
|
|
60
|
-
const ctl = createPullToRefresh({ scroller, indicator, cfg })
|
|
58
|
+
const { scroller, indicator } = host()
|
|
59
|
+
const cfg = { current: { onRefresh: vi.fn(), refreshing: false } }
|
|
60
|
+
const ctl = createPullToRefresh({ scroller, indicator, cfg })
|
|
61
61
|
|
|
62
|
-
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
63
|
-
scroller.dispatchEvent(pointer('pointermove', 320))
|
|
64
|
-
scroller.dispatchEvent(pointer('pointerup', 320))
|
|
62
|
+
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
63
|
+
scroller.dispatchEvent(pointer('pointermove', 320))
|
|
64
|
+
scroller.dispatchEvent(pointer('pointerup', 320))
|
|
65
65
|
// 220 * 0.5 = 110 >= 64 → docked
|
|
66
|
-
expect(cfg.current.onRefresh).toHaveBeenCalledTimes(1)
|
|
67
|
-
expect(ty(scroller)).toContain(`translateY(${REFRESH_HEADER_HEIGHT}px)`)
|
|
68
|
-
expect(ty(indicator)).toContain('translateY(0px)')
|
|
66
|
+
expect(cfg.current.onRefresh).toHaveBeenCalledTimes(1)
|
|
67
|
+
expect(ty(scroller)).toContain(`translateY(${REFRESH_HEADER_HEIGHT}px)`)
|
|
68
|
+
expect(ty(indicator)).toContain('translateY(0px)')
|
|
69
69
|
|
|
70
70
|
// refreshing never arrives → grace collapse
|
|
71
|
-
vi.advanceTimersByTime(400)
|
|
72
|
-
expect(ty(scroller)).toContain('translateY(0px)')
|
|
73
|
-
ctl.detach()
|
|
74
|
-
})
|
|
71
|
+
vi.advanceTimersByTime(400)
|
|
72
|
+
expect(ty(scroller)).toContain('translateY(0px)')
|
|
73
|
+
ctl.detach()
|
|
74
|
+
})
|
|
75
75
|
|
|
76
76
|
it('refreshing keeps the dock; sync(false) collapses', () => {
|
|
77
|
-
const { scroller, indicator } = host()
|
|
78
|
-
const cfg = { current: { onRefresh: vi.fn(), refreshing: false } }
|
|
79
|
-
const ctl = createPullToRefresh({ scroller, indicator, cfg })
|
|
80
|
-
|
|
81
|
-
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
82
|
-
scroller.dispatchEvent(pointer('pointermove', 320))
|
|
83
|
-
cfg.current.refreshing = true
|
|
84
|
-
ctl.sync()
|
|
85
|
-
scroller.dispatchEvent(pointer('pointerup', 320))
|
|
86
|
-
expect(cfg.current.onRefresh).toHaveBeenCalledTimes(1)
|
|
87
|
-
|
|
88
|
-
vi.advanceTimersByTime(400)
|
|
89
|
-
expect(ty(scroller)).toContain(`translateY(${REFRESH_HEADER_HEIGHT}px)`)
|
|
90
|
-
|
|
91
|
-
cfg.current.refreshing = false
|
|
92
|
-
ctl.sync()
|
|
93
|
-
expect(ty(scroller)).toContain('translateY(0px)')
|
|
94
|
-
expect(ty(indicator)).toContain(`translateY(${-REFRESH_HEADER_HEIGHT}px)`)
|
|
95
|
-
ctl.detach()
|
|
96
|
-
})
|
|
77
|
+
const { scroller, indicator } = host()
|
|
78
|
+
const cfg = { current: { onRefresh: vi.fn(), refreshing: false } }
|
|
79
|
+
const ctl = createPullToRefresh({ scroller, indicator, cfg })
|
|
80
|
+
|
|
81
|
+
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
82
|
+
scroller.dispatchEvent(pointer('pointermove', 320))
|
|
83
|
+
cfg.current.refreshing = true
|
|
84
|
+
ctl.sync()
|
|
85
|
+
scroller.dispatchEvent(pointer('pointerup', 320))
|
|
86
|
+
expect(cfg.current.onRefresh).toHaveBeenCalledTimes(1)
|
|
87
|
+
|
|
88
|
+
vi.advanceTimersByTime(400)
|
|
89
|
+
expect(ty(scroller)).toContain(`translateY(${REFRESH_HEADER_HEIGHT}px)`)
|
|
90
|
+
|
|
91
|
+
cfg.current.refreshing = false
|
|
92
|
+
ctl.sync()
|
|
93
|
+
expect(ty(scroller)).toContain('translateY(0px)')
|
|
94
|
+
expect(ty(indicator)).toContain(`translateY(${-REFRESH_HEADER_HEIGHT}px)`)
|
|
95
|
+
ctl.detach()
|
|
96
|
+
})
|
|
97
97
|
|
|
98
98
|
it('ignores drags that start below the top edge', () => {
|
|
99
|
-
const { scroller } = host()
|
|
100
|
-
Object.defineProperty(scroller, 'scrollTop', { value: 30, writable: true })
|
|
101
|
-
const cfg = { current: { onRefresh: vi.fn() } }
|
|
102
|
-
const ctl = createPullToRefresh({ scroller, indicator: document.createElement('div'), cfg })
|
|
103
|
-
|
|
104
|
-
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
105
|
-
scroller.dispatchEvent(pointer('pointermove', 320))
|
|
106
|
-
scroller.dispatchEvent(pointer('pointerup', 320))
|
|
107
|
-
expect(cfg.current.onRefresh).not.toHaveBeenCalled()
|
|
108
|
-
expect(ty(scroller)).toBe('')
|
|
109
|
-
ctl.detach()
|
|
110
|
-
})
|
|
111
|
-
})
|
|
99
|
+
const { scroller } = host()
|
|
100
|
+
Object.defineProperty(scroller, 'scrollTop', { value: 30, writable: true })
|
|
101
|
+
const cfg = { current: { onRefresh: vi.fn() } }
|
|
102
|
+
const ctl = createPullToRefresh({ scroller, indicator: document.createElement('div'), cfg })
|
|
103
|
+
|
|
104
|
+
scroller.dispatchEvent(pointer('pointerdown', 100))
|
|
105
|
+
scroller.dispatchEvent(pointer('pointermove', 320))
|
|
106
|
+
scroller.dispatchEvent(pointer('pointerup', 320))
|
|
107
|
+
expect(cfg.current.onRefresh).not.toHaveBeenCalled()
|
|
108
|
+
expect(ty(scroller)).toBe('')
|
|
109
|
+
ctl.detach()
|
|
110
|
+
})
|
|
111
|
+
})
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { beforeEach, expect, it, vi } from 'vitest'
|
|
2
|
+
import { defineRoutes } from './route-table'
|
|
3
|
+
const native = vi.hoisted(() => {
|
|
4
|
+
class View {
|
|
5
|
+
isLoaded = true
|
|
6
|
+
frame: any
|
|
7
|
+
actionBar = { title: '' }
|
|
8
|
+
modal: any = null
|
|
9
|
+
closeCallback: (() => void) | undefined
|
|
10
|
+
showModal(host: any, options: any) {
|
|
11
|
+
this.modal = host
|
|
12
|
+
host.closeCallback = () => {
|
|
13
|
+
this.modal = null
|
|
14
|
+
options.closeCallback()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
closeModal() {
|
|
18
|
+
this.closeCallback?.()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
class Frame extends View {
|
|
23
|
+
currentPage: any = new View()
|
|
24
|
+
backStack: any[] = []
|
|
25
|
+
listeners = new Map<string, () => void>()
|
|
26
|
+
constructor() {
|
|
27
|
+
super()
|
|
28
|
+
this.currentPage.frame = this
|
|
29
|
+
}
|
|
30
|
+
on(name: string, cb: () => void) {
|
|
31
|
+
this.listeners.set(name, cb)
|
|
32
|
+
}
|
|
33
|
+
navigate({ create }: any) {
|
|
34
|
+
this.backStack.push(this.currentPage)
|
|
35
|
+
this.currentPage = create()
|
|
36
|
+
this.currentPage.frame = this
|
|
37
|
+
this.listeners.get('navigatedTo')?.()
|
|
38
|
+
}
|
|
39
|
+
goBack() {
|
|
40
|
+
if (this.backStack.length) {
|
|
41
|
+
this.currentPage = this.backStack.pop()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.listeners.get('navigatedTo')?.()
|
|
45
|
+
}
|
|
46
|
+
callLoaded() {
|
|
47
|
+
this.isLoaded = true
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const events = new Map<string, (event: any) => void>()
|
|
52
|
+
const app: any = {
|
|
53
|
+
android: { on: (name: string, cb: any) => events.set(name, cb) },
|
|
54
|
+
AndroidApplication: { activityBackPressedEvent: 'back' },
|
|
55
|
+
getRootView: () => null,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { View, Frame, app, events, roots: [] as any[] }
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
vi.mock('@nativescript/core', () => ({
|
|
62
|
+
Application: native.app,
|
|
63
|
+
Frame: native.Frame,
|
|
64
|
+
Page: native.View,
|
|
65
|
+
GridLayout: native.View,
|
|
66
|
+
}))
|
|
67
|
+
|
|
68
|
+
vi.mock('octane', () => ({ useSyncExternalStore: vi.fn(), hookSlots: vi.fn() }))
|
|
69
|
+
vi.mock('@nativescript-community/octane', () => ({
|
|
70
|
+
createNativeScriptRoot: (host: any) => {
|
|
71
|
+
const root = { host, render: vi.fn(), unmount: vi.fn() }
|
|
72
|
+
native.roots.push(root)
|
|
73
|
+
return root
|
|
74
|
+
},
|
|
75
|
+
}))
|
|
76
|
+
|
|
77
|
+
vi.mock('./route-host.mobile', () => ({ RouteHost: () => null }))
|
|
78
|
+
vi.mock('./stacks', async () => import('./stacks.ts'))
|
|
79
|
+
let router: typeof import('./route')
|
|
80
|
+
let stacks: typeof import('./stacks')
|
|
81
|
+
let frame: InstanceType<typeof native.Frame>
|
|
82
|
+
beforeEach(async () => {
|
|
83
|
+
vi.resetModules()
|
|
84
|
+
delete native.app.__octaneXplatBackWired
|
|
85
|
+
delete native.app.__octaneXplatBackPress
|
|
86
|
+
native.events.clear()
|
|
87
|
+
native.roots.length = 0
|
|
88
|
+
router = await import('./route.ts')
|
|
89
|
+
stacks = await import('./stacks.ts')
|
|
90
|
+
frame = new native.Frame()
|
|
91
|
+
stacks.registerStack('root', frame as any)
|
|
92
|
+
router.registerRoutes(defineRoutes([{ path: 'detail', screen: () => null }]))
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const push = (stack: string, id: string, presentation?: 'modal') =>
|
|
96
|
+
router.pushRoute({ stack, name: 'detail', params: { id }, presentation })
|
|
97
|
+
|
|
98
|
+
const back = () => {
|
|
99
|
+
const event = { cancel: false }
|
|
100
|
+
native.events.get('back')!(event)
|
|
101
|
+
return event.cancel
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
it('uses route arrays even when Android has a registered named Frame', () => {
|
|
105
|
+
const named = new native.Frame()
|
|
106
|
+
stacks.registerStack('first', named as any)
|
|
107
|
+
push('first', 'one')
|
|
108
|
+
push('first', 'two')
|
|
109
|
+
expect(named.backStack).toHaveLength(0)
|
|
110
|
+
expect(router.routeFor('first')?.params.id).toBe('two')
|
|
111
|
+
router.popRoute('first')
|
|
112
|
+
expect(router.routeFor('first')?.params.id).toBe('one')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('orders interceptors then modal, root, latest named stack and base fall-through', () => {
|
|
116
|
+
push('first', 'one')
|
|
117
|
+
push('second', 'two')
|
|
118
|
+
push('root', 'root')
|
|
119
|
+
push('root', 'modal', 'modal')
|
|
120
|
+
const calls: string[] = []
|
|
121
|
+
const older = router.addBackInterceptor(() => {
|
|
122
|
+
calls.push('old')
|
|
123
|
+
return false
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const newest = router.addBackInterceptor(() => {
|
|
127
|
+
calls.push('new')
|
|
128
|
+
return true
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
expect(back()).toBe(true)
|
|
132
|
+
expect(calls).toEqual(['new'])
|
|
133
|
+
newest()
|
|
134
|
+
expect(back()).toBe(true)
|
|
135
|
+
expect(calls).toEqual(['new', 'old'])
|
|
136
|
+
expect(router.currentModalRoute()).toBeNull()
|
|
137
|
+
expect(router.routeFor('root')?.params.id).toBe('root')
|
|
138
|
+
older()
|
|
139
|
+
expect(back()).toBe(true)
|
|
140
|
+
expect(router.routeFor('root')).toBeNull()
|
|
141
|
+
expect(back()).toBe(true)
|
|
142
|
+
expect(router.routeFor('second')).toBeNull()
|
|
143
|
+
expect(router.routeFor('first')?.params.id).toBe('one')
|
|
144
|
+
expect(back()).toBe(true)
|
|
145
|
+
expect(back()).toBe(false)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('unmounts the separate modal root exactly once when dismissed', () => {
|
|
149
|
+
push('root', 'modal', 'modal')
|
|
150
|
+
const modalRoot = native.roots.at(-1)
|
|
151
|
+
router.popRoute()
|
|
152
|
+
expect(modalRoot.unmount).toHaveBeenCalledTimes(1)
|
|
153
|
+
expect(router.currentModalRoute()).toBeNull()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('unmounts when the modal host invokes its close callback', () => {
|
|
157
|
+
push('root', 'modal', 'modal')
|
|
158
|
+
const modalRoot = native.roots.at(-1)
|
|
159
|
+
modalRoot.host.closeModal()
|
|
160
|
+
expect(modalRoot.unmount).toHaveBeenCalledTimes(1)
|
|
161
|
+
expect(router.currentModalRoute()).toBeNull()
|
|
162
|
+
})
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeEach, expect, it, vi } from 'vitest'
|
|
3
|
+
import { defineRoutes } from './route-table'
|
|
4
|
+
vi.mock('octane', () => ({ useSyncExternalStore: vi.fn() }))
|
|
5
|
+
|
|
6
|
+
let router: typeof import('./route.web')
|
|
7
|
+
beforeEach(async () => {
|
|
8
|
+
vi.resetModules()
|
|
9
|
+
vi.spyOn(window, 'scrollTo').mockImplementation(() => {})
|
|
10
|
+
history.replaceState(null, '', '/')
|
|
11
|
+
router = await import('./route.web')
|
|
12
|
+
router.registerRoutes(
|
|
13
|
+
defineRoutes([
|
|
14
|
+
{
|
|
15
|
+
path: 'detail',
|
|
16
|
+
screen: () => null,
|
|
17
|
+
beforeLoad: () => ({ guarded: true }),
|
|
18
|
+
loader: () => 'loaded',
|
|
19
|
+
},
|
|
20
|
+
{ path: 'about', screen: () => null, presentation: 'modal' },
|
|
21
|
+
]),
|
|
22
|
+
)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 25))
|
|
26
|
+
|
|
27
|
+
it('restores guard context and loader data on back and forward', async () => {
|
|
28
|
+
router.pushRoute({ stack: 'root', name: 'detail', params: {} })
|
|
29
|
+
await settle()
|
|
30
|
+
router.pushRoute({ stack: 'root', name: 'about', params: {} })
|
|
31
|
+
history.back()
|
|
32
|
+
await settle()
|
|
33
|
+
expect(router.routeFor('root')).toMatchObject({
|
|
34
|
+
context: { guarded: true },
|
|
35
|
+
loaderData: 'loaded',
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
history.forward()
|
|
39
|
+
await settle()
|
|
40
|
+
expect(router.currentModalRoute()?.name).toBe('about')
|
|
41
|
+
expect(router.routeFor('root')?.name).toBe('detail')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('keeps loaded route state when addRoutes updates the registry', async () => {
|
|
45
|
+
router.pushRoute({ stack: 'root', name: 'detail', params: {} })
|
|
46
|
+
await settle()
|
|
47
|
+
router.addRoutes(defineRoutes([{ path: 'extra', screen: () => null }]))
|
|
48
|
+
expect(router.routeFor('root')).toMatchObject({
|
|
49
|
+
context: { guarded: true },
|
|
50
|
+
loaderData: 'loaded',
|
|
51
|
+
})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('counts the visible modal as a back affordance from the base shell', () => {
|
|
55
|
+
router.pushRoute({ stack: 'root', name: 'about', params: {} })
|
|
56
|
+
expect(router.canGoBack()).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('rejects unmatched incoming links without changing history', () => {
|
|
60
|
+
const length = history.length
|
|
61
|
+
expect(router.pushDeepLink('xplat://missing')).toBe(false)
|
|
62
|
+
expect(history.length).toBe(length)
|
|
63
|
+
expect(router.currentRoute()).toBeNull()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('loads a direct URL without adding history or running its guard', async () => {
|
|
67
|
+
const guard = vi.fn()
|
|
68
|
+
const loader = vi.fn(() => ({ entries: ['baked'] }))
|
|
69
|
+
history.replaceState(null, '', '/baked')
|
|
70
|
+
const length = history.length
|
|
71
|
+
router.registerRoutes(
|
|
72
|
+
defineRoutes([{ path: 'baked', screen: () => null, beforeLoad: guard, loader }]),
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
await settle()
|
|
76
|
+
expect(router.routeFor('root')).toMatchObject({ loaderData: { entries: ['baked'] } })
|
|
77
|
+
expect(history.length).toBe(length)
|
|
78
|
+
expect(guard).not.toHaveBeenCalled()
|
|
79
|
+
expect(loader).toHaveBeenCalledTimes(1)
|
|
80
|
+
})
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { beforeAll, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import type { Route, RouteManifest } from './props'
|
|
4
|
+
|
|
5
|
+
const registration = vi.hoisted(() => ({
|
|
6
|
+
listener: null as null | ((name: string, frame: any) => void),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
vi.mock('@nativescript/core', () => ({
|
|
10
|
+
Application: { android: null },
|
|
11
|
+
Frame: class {},
|
|
12
|
+
GridLayout: class {},
|
|
13
|
+
Page: class {},
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
vi.mock('@nativescript-community/octane', () => ({
|
|
17
|
+
createNativeScriptRoot: () => ({ render() {}, unmount() {} }),
|
|
18
|
+
}))
|
|
19
|
+
|
|
20
|
+
vi.mock('./stacks', () => ({
|
|
21
|
+
getStack: () => undefined,
|
|
22
|
+
onStackRegistered(listener: typeof registration.listener) {
|
|
23
|
+
registration.listener = listener
|
|
24
|
+
},
|
|
25
|
+
stackEntries: () => new Map().entries(),
|
|
26
|
+
}))
|
|
27
|
+
|
|
28
|
+
vi.mock('./route-host.mobile', () => ({ RouteHost: () => null }))
|
|
29
|
+
|
|
30
|
+
function deferred<T>() {
|
|
31
|
+
let resolve!: (value: T) => void
|
|
32
|
+
let reject!: (error: unknown) => void
|
|
33
|
+
const promise = new Promise<T>((yes, no) => {
|
|
34
|
+
resolve = yes
|
|
35
|
+
reject = no
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return { promise, resolve, reject }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const settle = () => new Promise((resolve) => setTimeout(resolve, 0))
|
|
42
|
+
|
|
43
|
+
describe.each(['web', 'macos', 'native'] as const)('%s navigation ownership', (target) => {
|
|
44
|
+
let router:
|
|
45
|
+
| typeof import('./route.web')
|
|
46
|
+
| typeof import('./route.macos')
|
|
47
|
+
| typeof import('./route')
|
|
48
|
+
|
|
49
|
+
beforeAll(async () => {
|
|
50
|
+
router =
|
|
51
|
+
target === 'web'
|
|
52
|
+
? await import('./route.web')
|
|
53
|
+
: target === 'macos'
|
|
54
|
+
? await import('./route.macos')
|
|
55
|
+
: await import('./route.ts')
|
|
56
|
+
}, 60000)
|
|
57
|
+
|
|
58
|
+
function setup(slow: Partial<RouteManifest['routes'][number]> = {}) {
|
|
59
|
+
router.registerRoutes({
|
|
60
|
+
screens: { slow: () => null, fast: () => null, redirected: () => null },
|
|
61
|
+
layouts: {},
|
|
62
|
+
routes: ['slow', 'fast', 'redirected'].map((name) => ({
|
|
63
|
+
name,
|
|
64
|
+
file: name,
|
|
65
|
+
segments: [name],
|
|
66
|
+
params: [],
|
|
67
|
+
...(name === 'slow' ? slow : {}),
|
|
68
|
+
})),
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const route = (name: string, stack = 'audit'): Route => ({ name, stack, params: {} })
|
|
73
|
+
if (target === 'native') {
|
|
74
|
+
it('invalidates pending work when a newly registered frame goes back', async () => {
|
|
75
|
+
const wait = deferred<unknown>()
|
|
76
|
+
setup({ loader: () => wait.promise })
|
|
77
|
+
const on = vi.fn()
|
|
78
|
+
registration.listener!('audit', { on, currentPage: null })
|
|
79
|
+
router.pushRoute(route('fast'))
|
|
80
|
+
router.pushRoute(route('slow'))
|
|
81
|
+
on.mock.calls[0][1]({ isBackNavigation: true })
|
|
82
|
+
wait.resolve('old')
|
|
83
|
+
await settle()
|
|
84
|
+
expect(router.routeFor('audit')?.name).toBe('fast')
|
|
85
|
+
})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
it('ignores a superseded loader result', async () => {
|
|
89
|
+
const wait = deferred<unknown>()
|
|
90
|
+
setup({ loader: () => wait.promise })
|
|
91
|
+
router.pushRoute(route('slow'))
|
|
92
|
+
await settle()
|
|
93
|
+
router.pushRoute(route('fast'))
|
|
94
|
+
wait.resolve('old')
|
|
95
|
+
await settle()
|
|
96
|
+
expect(router.routeFor('audit')?.name).toBe('fast')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('ignores a superseded loader rejection', async () => {
|
|
100
|
+
const wait = deferred<unknown>()
|
|
101
|
+
setup({ loader: () => wait.promise })
|
|
102
|
+
router.pushRoute(route('slow'))
|
|
103
|
+
await settle()
|
|
104
|
+
router.pushRoute(route('fast'))
|
|
105
|
+
wait.reject(new Error('old'))
|
|
106
|
+
await settle()
|
|
107
|
+
expect(router.routeFor('audit')?.name).toBe('fast')
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('ignores a superseded guard and its redirect', async () => {
|
|
111
|
+
const wait = deferred<unknown>()
|
|
112
|
+
setup({
|
|
113
|
+
beforeLoad: async () => {
|
|
114
|
+
await wait.promise
|
|
115
|
+
router.redirect(route('redirected'))
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
router.pushRoute(route('slow'))
|
|
120
|
+
router.pushRoute(route('fast'))
|
|
121
|
+
wait.resolve(null)
|
|
122
|
+
await settle()
|
|
123
|
+
expect(router.routeFor('audit')?.name).toBe('fast')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('preserves active guard context and loader data', async () => {
|
|
127
|
+
setup({ beforeLoad: async () => ({ allowed: true }), loader: async () => 'loaded' })
|
|
128
|
+
router.pushRoute(route('slow'))
|
|
129
|
+
await settle()
|
|
130
|
+
expect(router.routeFor('audit')).toMatchObject({
|
|
131
|
+
name: 'slow',
|
|
132
|
+
context: { allowed: true },
|
|
133
|
+
loaderData: 'loaded',
|
|
134
|
+
})
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('preserves an active redirect', async () => {
|
|
138
|
+
setup({ beforeLoad: async () => router.redirect(route('redirected')) })
|
|
139
|
+
router.pushRoute(route('slow'))
|
|
140
|
+
await settle()
|
|
141
|
+
expect(router.routeFor('audit')?.name).toBe('redirected')
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
it('invalidates pending work when back is requested', async () => {
|
|
145
|
+
const wait = deferred<unknown>()
|
|
146
|
+
setup({ loader: () => wait.promise })
|
|
147
|
+
router.pushRoute(route('fast'))
|
|
148
|
+
router.pushRoute(route('slow'))
|
|
149
|
+
await settle()
|
|
150
|
+
if (target === 'web') {
|
|
151
|
+
vi.spyOn(history, 'back').mockImplementation(() => {})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
router.popRoute('audit')
|
|
155
|
+
const before = router.routeFor('audit')
|
|
156
|
+
wait.resolve('old')
|
|
157
|
+
await settle()
|
|
158
|
+
expect(router.routeFor('audit')).toEqual(before)
|
|
159
|
+
vi.restoreAllMocks()
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('uses one history on web and independent named stacks elsewhere', async () => {
|
|
163
|
+
const wait = deferred<unknown>()
|
|
164
|
+
setup({ loader: () => wait.promise })
|
|
165
|
+
router.pushRoute(route('slow'))
|
|
166
|
+
await settle()
|
|
167
|
+
router.pushRoute(route('fast', 'other'))
|
|
168
|
+
wait.resolve('loaded')
|
|
169
|
+
await settle()
|
|
170
|
+
expect(router.routeFor('audit')?.name ?? null).toBe(target === 'web' ? null : 'slow')
|
|
171
|
+
expect(router.routeFor('other')?.name ?? null).toBe('fast')
|
|
172
|
+
})
|
|
173
|
+
})
|