@flighthq/gui 0.4.1-edge.28.7f8e221

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 (96) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +17 -0
  3. package/dist/buttonController.d.ts +7 -0
  4. package/dist/buttonController.d.ts.map +1 -0
  5. package/dist/buttonController.js +83 -0
  6. package/dist/buttonController.js.map +1 -0
  7. package/dist/comboBoxController.d.ts +7 -0
  8. package/dist/comboBoxController.d.ts.map +1 -0
  9. package/dist/comboBoxController.js +46 -0
  10. package/dist/comboBoxController.js.map +1 -0
  11. package/dist/contract.d.ts +16 -0
  12. package/dist/contract.d.ts.map +1 -0
  13. package/dist/contract.js +16 -0
  14. package/dist/contract.js.map +1 -0
  15. package/dist/guiController.d.ts +24 -0
  16. package/dist/guiController.d.ts.map +1 -0
  17. package/dist/guiController.js +101 -0
  18. package/dist/guiController.js.map +1 -0
  19. package/dist/guiTestHelper.d.ts +5 -0
  20. package/dist/guiTestHelper.d.ts.map +1 -0
  21. package/dist/guiTestHelper.js +57 -0
  22. package/dist/guiTestHelper.js.map +1 -0
  23. package/dist/index.d.ts +2 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/listController.d.ts +8 -0
  28. package/dist/listController.d.ts.map +1 -0
  29. package/dist/listController.js +82 -0
  30. package/dist/listController.js.map +1 -0
  31. package/dist/progressBarController.d.ts +6 -0
  32. package/dist/progressBarController.d.ts.map +1 -0
  33. package/dist/progressBarController.js +42 -0
  34. package/dist/progressBarController.js.map +1 -0
  35. package/dist/radioGroupController.d.ts +7 -0
  36. package/dist/radioGroupController.d.ts.map +1 -0
  37. package/dist/radioGroupController.js +50 -0
  38. package/dist/radioGroupController.js.map +1 -0
  39. package/dist/scrollBarController.d.ts +7 -0
  40. package/dist/scrollBarController.d.ts.map +1 -0
  41. package/dist/scrollBarController.js +124 -0
  42. package/dist/scrollBarController.js.map +1 -0
  43. package/dist/scrollViewController.d.ts +8 -0
  44. package/dist/scrollViewController.d.ts.map +1 -0
  45. package/dist/scrollViewController.js +99 -0
  46. package/dist/scrollViewController.js.map +1 -0
  47. package/dist/sliderController.d.ts +7 -0
  48. package/dist/sliderController.d.ts.map +1 -0
  49. package/dist/sliderController.js +105 -0
  50. package/dist/sliderController.js.map +1 -0
  51. package/dist/splitPaneController.d.ts +7 -0
  52. package/dist/splitPaneController.d.ts.map +1 -0
  53. package/dist/splitPaneController.js +87 -0
  54. package/dist/splitPaneController.js.map +1 -0
  55. package/dist/tabBarController.d.ts +7 -0
  56. package/dist/tabBarController.d.ts.map +1 -0
  57. package/dist/tabBarController.js +43 -0
  58. package/dist/tabBarController.js.map +1 -0
  59. package/dist/textInputController.d.ts +9 -0
  60. package/dist/textInputController.d.ts.map +1 -0
  61. package/dist/textInputController.js +132 -0
  62. package/dist/textInputController.js.map +1 -0
  63. package/dist/toggleController.d.ts +7 -0
  64. package/dist/toggleController.d.ts.map +1 -0
  65. package/dist/toggleController.js +57 -0
  66. package/dist/toggleController.js.map +1 -0
  67. package/dist/tooltipController.d.ts +6 -0
  68. package/dist/tooltipController.d.ts.map +1 -0
  69. package/dist/tooltipController.js +62 -0
  70. package/dist/tooltipController.js.map +1 -0
  71. package/dist/treeViewController.d.ts +10 -0
  72. package/dist/treeViewController.d.ts.map +1 -0
  73. package/dist/treeViewController.js +128 -0
  74. package/dist/treeViewController.js.map +1 -0
  75. package/dist/windowController.d.ts +7 -0
  76. package/dist/windowController.d.ts.map +1 -0
  77. package/dist/windowController.js +119 -0
  78. package/dist/windowController.js.map +1 -0
  79. package/package.json +53 -0
  80. package/src/buttonController.test.ts +90 -0
  81. package/src/comboBoxController.test.ts +71 -0
  82. package/src/guiController.test.ts +162 -0
  83. package/src/guiTreeShaking.test.ts +35 -0
  84. package/src/listController.test.ts +96 -0
  85. package/src/progressBarController.test.ts +49 -0
  86. package/src/radioGroupController.test.ts +61 -0
  87. package/src/scrollBarController.test.ts +86 -0
  88. package/src/scrollViewController.test.ts +80 -0
  89. package/src/sliderController.test.ts +74 -0
  90. package/src/splitPaneController.test.ts +85 -0
  91. package/src/tabBarController.test.ts +50 -0
  92. package/src/textInputController.test.ts +93 -0
  93. package/src/toggleController.test.ts +72 -0
  94. package/src/tooltipController.test.ts +56 -0
  95. package/src/treeViewController.test.ts +105 -0
  96. package/src/windowController.test.ts +75 -0
@@ -0,0 +1,49 @@
1
+ import { createGuiTestNode } from './guiTestHelper';
2
+ import {
3
+ createProgressBarController,
4
+ disposeProgressBarController,
5
+ getProgressBarControllerValue,
6
+ setProgressBarControllerValue,
7
+ } from './progressBarController';
8
+
9
+ describe('createProgressBarController', () => {
10
+ it('scales the caller fill without changing the track', () => {
11
+ const fill = createGuiTestNode();
12
+ const track = createGuiTestNode();
13
+ createProgressBarController({ fill, maximum: 100, track, value: 25 });
14
+ expect(fill.scaleX).toBe(0.25);
15
+ expect(track.scaleX).toBe(1);
16
+ });
17
+ });
18
+
19
+ describe('disposeProgressBarController', () => {
20
+ it('is idempotent', () => {
21
+ const controller = createProgressBarController({ fill: createGuiTestNode(), track: createGuiTestNode() });
22
+ disposeProgressBarController(controller);
23
+ expect(() => disposeProgressBarController(controller)).not.toThrow();
24
+ });
25
+ });
26
+
27
+ describe('getProgressBarControllerValue', () => {
28
+ it('returns a clamped initial value', () => {
29
+ expect(
30
+ getProgressBarControllerValue(
31
+ createProgressBarController({ fill: createGuiTestNode(), maximum: 10, track: createGuiTestNode(), value: 20 }),
32
+ ),
33
+ ).toBe(10);
34
+ });
35
+ });
36
+
37
+ describe('setProgressBarControllerValue', () => {
38
+ it('updates vertical fill scale', () => {
39
+ const fill = createGuiTestNode();
40
+ const controller = createProgressBarController({
41
+ fill,
42
+ maximum: 10,
43
+ orientation: 'vertical',
44
+ track: createGuiTestNode(),
45
+ });
46
+ setProgressBarControllerValue(controller, 5);
47
+ expect(fill.scaleY).toBe(0.5);
48
+ });
49
+ });
@@ -0,0 +1,61 @@
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+
3
+ import { createGuiTestNode } from './guiTestHelper';
4
+ import {
5
+ createRadioGroupController,
6
+ disposeRadioGroupController,
7
+ getRadioGroupControllerSelectedIndex,
8
+ getRadioGroupControllerSignals,
9
+ setRadioGroupControllerSelectedIndex,
10
+ } from './radioGroupController';
11
+ import { createToggleController, isToggleControllerChecked, setToggleControllerChecked } from './toggleController';
12
+
13
+ function toggle() {
14
+ return createToggleController({ checkedState: createGuiTestNode(), uncheckedState: createGuiTestNode() });
15
+ }
16
+
17
+ describe('createRadioGroupController', () => {
18
+ it('enforces mutual exclusion from toggle changes', () => {
19
+ const toggles = [toggle(), toggle(), toggle()];
20
+ const controller = createRadioGroupController({ selectedIndex: 0, toggles });
21
+ setToggleControllerChecked(toggles[2], true);
22
+ expect(getRadioGroupControllerSelectedIndex(controller)).toBe(2);
23
+ expect(toggles.map(isToggleControllerChecked)).toEqual([false, false, true]);
24
+ });
25
+ });
26
+
27
+ describe('disposeRadioGroupController', () => {
28
+ it('stops observing member toggles', () => {
29
+ const member = toggle();
30
+ const controller = createRadioGroupController({ selectedIndex: -1, toggles: [member] });
31
+ disposeRadioGroupController(controller);
32
+ setToggleControllerChecked(member, true);
33
+ expect(getRadioGroupControllerSelectedIndex(controller)).toBe(-1);
34
+ });
35
+ });
36
+
37
+ describe('getRadioGroupControllerSelectedIndex', () => {
38
+ it('normalizes out-of-range values to -1', () => {
39
+ expect(
40
+ getRadioGroupControllerSelectedIndex(createRadioGroupController({ selectedIndex: 4, toggles: [toggle()] })),
41
+ ).toBe(-1);
42
+ });
43
+ });
44
+
45
+ describe('getRadioGroupControllerSignals', () => {
46
+ it('emits selected indices', () => {
47
+ const controller = createRadioGroupController({ toggles: [toggle(), toggle()] });
48
+ const values: number[] = [];
49
+ connectSignal(getRadioGroupControllerSignals(controller).onChange, (value) => values.push(value));
50
+ setRadioGroupControllerSelectedIndex(controller, 1);
51
+ expect(values).toEqual([1]);
52
+ });
53
+ });
54
+
55
+ describe('setRadioGroupControllerSelectedIndex', () => {
56
+ it('can clear the selection', () => {
57
+ const controller = createRadioGroupController({ toggles: [toggle()] });
58
+ setRadioGroupControllerSelectedIndex(controller, -1);
59
+ expect(getRadioGroupControllerSelectedIndex(controller)).toBe(-1);
60
+ });
61
+ });
@@ -0,0 +1,86 @@
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+
3
+ import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
4
+ import {
5
+ createScrollBarController,
6
+ disposeScrollBarController,
7
+ getScrollBarControllerSignals,
8
+ getScrollBarControllerValue,
9
+ setScrollBarControllerValue,
10
+ } from './scrollBarController';
11
+
12
+ describe('createScrollBarController', () => {
13
+ it('pages on track clicks and positions the thumb', () => {
14
+ const track = createGuiTestNode(20, 100);
15
+ const thumb = createGuiTestNode(20, 10);
16
+ const controller = createScrollBarController({ maximum: 100, pageSize: 20, thumb, track });
17
+ emitGuiPointer(track, 'onPointerDown', { localY: 80 });
18
+ expect(getScrollBarControllerValue(controller)).toBe(20);
19
+ expect(thumb.y).toBe(18);
20
+ });
21
+
22
+ it('repeat-clicks optional line buttons and cancels the timer', () => {
23
+ vi.useFakeTimers();
24
+ const down = createGuiTestNode();
25
+ const controller = createScrollBarController({
26
+ downButton: down,
27
+ maximum: 10,
28
+ thumb: createGuiTestNode(),
29
+ track: createGuiTestNode(),
30
+ });
31
+ emitGuiPointer(down, 'onPointerDown');
32
+ vi.advanceTimersByTime(250);
33
+ emitGuiPointer(down, 'onPointerUp');
34
+ expect(getScrollBarControllerValue(controller)).toBe(3);
35
+ vi.useRealTimers();
36
+ });
37
+ });
38
+
39
+ describe('disposeScrollBarController', () => {
40
+ it('cancels repeat timers', () => {
41
+ vi.useFakeTimers();
42
+ const down = createGuiTestNode();
43
+ const controller = createScrollBarController({
44
+ downButton: down,
45
+ thumb: createGuiTestNode(),
46
+ track: createGuiTestNode(),
47
+ });
48
+ emitGuiPointer(down, 'onPointerDown');
49
+ disposeScrollBarController(controller);
50
+ vi.advanceTimersByTime(500);
51
+ expect(getScrollBarControllerValue(controller)).toBe(1);
52
+ vi.useRealTimers();
53
+ });
54
+ });
55
+
56
+ describe('getScrollBarControllerSignals', () => {
57
+ it('emits changes', () => {
58
+ const controller = createScrollBarController({ thumb: createGuiTestNode(), track: createGuiTestNode() });
59
+ const values: number[] = [];
60
+ connectSignal(getScrollBarControllerSignals(controller).onChange, (value) => values.push(value));
61
+ setScrollBarControllerValue(controller, 5);
62
+ expect(values).toEqual([5]);
63
+ });
64
+ });
65
+
66
+ describe('getScrollBarControllerValue', () => {
67
+ it('returns the initial value', () => {
68
+ expect(
69
+ getScrollBarControllerValue(
70
+ createScrollBarController({ thumb: createGuiTestNode(), track: createGuiTestNode(), value: 5 }),
71
+ ),
72
+ ).toBe(5);
73
+ });
74
+ });
75
+
76
+ describe('setScrollBarControllerValue', () => {
77
+ it('clamps values', () => {
78
+ const controller = createScrollBarController({
79
+ maximum: 10,
80
+ thumb: createGuiTestNode(),
81
+ track: createGuiTestNode(),
82
+ });
83
+ setScrollBarControllerValue(controller, 20);
84
+ expect(getScrollBarControllerValue(controller)).toBe(10);
85
+ });
86
+ });
@@ -0,0 +1,80 @@
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+
3
+ import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
4
+ import {
5
+ createScrollViewController,
6
+ disposeScrollViewController,
7
+ getScrollViewControllerSignals,
8
+ getScrollViewControllerX,
9
+ getScrollViewControllerY,
10
+ setScrollViewControllerPosition,
11
+ } from './scrollViewController';
12
+
13
+ describe('createScrollViewController', () => {
14
+ it('pans content by drag and wheel within viewport bounds', () => {
15
+ const viewport = createGuiTestNode(100, 100);
16
+ const content = createGuiTestNode(300, 300);
17
+ const controller = createScrollViewController({ content, viewport });
18
+ emitGuiPointer(viewport, 'onPointerDown', { worldX: 50, worldY: 50 });
19
+ emitGuiPointer(viewport, 'onPointerMove', { worldX: 20, worldY: 10 });
20
+ expect([getScrollViewControllerX(controller), getScrollViewControllerY(controller)]).toEqual([30, 40]);
21
+ emitGuiPointer(viewport, 'onWheel', { deltaY: 20 });
22
+ expect(content.y).toBe(-60);
23
+ });
24
+ });
25
+
26
+ describe('disposeScrollViewController', () => {
27
+ it('detaches viewport input', () => {
28
+ const viewport = createGuiTestNode(100, 100);
29
+ const controller = createScrollViewController({ content: createGuiTestNode(200, 200), viewport });
30
+ disposeScrollViewController(controller);
31
+ emitGuiPointer(viewport, 'onWheel', { deltaY: 50 });
32
+ expect(getScrollViewControllerY(controller)).toBe(0);
33
+ });
34
+ });
35
+
36
+ describe('getScrollViewControllerSignals', () => {
37
+ it('emits both axes', () => {
38
+ const controller = createScrollViewController({
39
+ content: createGuiTestNode(200, 200),
40
+ viewport: createGuiTestNode(),
41
+ });
42
+ const values: number[][] = [];
43
+ connectSignal(getScrollViewControllerSignals(controller).onScroll, (x, y) => values.push([x, y]));
44
+ setScrollViewControllerPosition(controller, 10, 20);
45
+ expect(values).toEqual([[10, 20]]);
46
+ });
47
+ });
48
+
49
+ describe('getScrollViewControllerX', () => {
50
+ it('returns horizontal offset', () => {
51
+ const controller = createScrollViewController({
52
+ content: createGuiTestNode(200, 200),
53
+ viewport: createGuiTestNode(),
54
+ });
55
+ setScrollViewControllerPosition(controller, 5, 0);
56
+ expect(getScrollViewControllerX(controller)).toBe(5);
57
+ });
58
+ });
59
+
60
+ describe('getScrollViewControllerY', () => {
61
+ it('returns vertical offset', () => {
62
+ const controller = createScrollViewController({
63
+ content: createGuiTestNode(200, 200),
64
+ viewport: createGuiTestNode(),
65
+ });
66
+ setScrollViewControllerPosition(controller, 0, 5);
67
+ expect(getScrollViewControllerY(controller)).toBe(5);
68
+ });
69
+ });
70
+
71
+ describe('setScrollViewControllerPosition', () => {
72
+ it('clamps to content extent', () => {
73
+ const controller = createScrollViewController({
74
+ content: createGuiTestNode(150, 150),
75
+ viewport: createGuiTestNode(100, 100),
76
+ });
77
+ setScrollViewControllerPosition(controller, 500, 500);
78
+ expect([getScrollViewControllerX(controller), getScrollViewControllerY(controller)]).toEqual([50, 50]);
79
+ });
80
+ });
@@ -0,0 +1,74 @@
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+
3
+ import { createGuiTestNode, emitGuiKeyboard, emitGuiPointer } from './guiTestHelper';
4
+ import {
5
+ createSliderController,
6
+ disposeSliderController,
7
+ getSliderControllerSignals,
8
+ getSliderControllerValue,
9
+ setSliderControllerValue,
10
+ } from './sliderController';
11
+
12
+ describe('createSliderController', () => {
13
+ it('maps track positions and thumb drags to values', () => {
14
+ const track = createGuiTestNode(100, 10);
15
+ const thumb = createGuiTestNode(10, 10);
16
+ const controller = createSliderController({ maximum: 10, thumb, track });
17
+ emitGuiPointer(track, 'onPointerDown', { localX: 50 });
18
+ expect(getSliderControllerValue(controller)).toBe(5);
19
+ expect(thumb.x).toBe(45);
20
+ emitGuiPointer(thumb, 'onPointerDown', { worldX: 45 });
21
+ emitGuiPointer(thumb, 'onPointerMove', { worldX: 90 });
22
+ expect(getSliderControllerValue(controller)).toBe(10);
23
+ });
24
+
25
+ it('supports keyboard selection', () => {
26
+ const track = createGuiTestNode();
27
+ const controller = createSliderController({ maximum: 10, step: 2, thumb: createGuiTestNode(), track });
28
+ emitGuiKeyboard(track, 'onKeyDown', 'ArrowRight');
29
+ expect(getSliderControllerValue(controller)).toBe(2);
30
+ });
31
+ });
32
+
33
+ describe('disposeSliderController', () => {
34
+ it('detaches track input', () => {
35
+ const track = createGuiTestNode();
36
+ const controller = createSliderController({ thumb: createGuiTestNode(), track });
37
+ disposeSliderController(controller);
38
+ emitGuiPointer(track, 'onPointerDown', { localX: 100 });
39
+ expect(getSliderControllerValue(controller)).toBe(0);
40
+ });
41
+ });
42
+
43
+ describe('getSliderControllerSignals', () => {
44
+ it('emits snapped values', () => {
45
+ const controller = createSliderController({
46
+ maximum: 10,
47
+ step: 2,
48
+ thumb: createGuiTestNode(),
49
+ track: createGuiTestNode(),
50
+ });
51
+ const values: number[] = [];
52
+ connectSignal(getSliderControllerSignals(controller).onChange, (value) => values.push(value));
53
+ setSliderControllerValue(controller, 3);
54
+ expect(values).toEqual([4]);
55
+ });
56
+ });
57
+
58
+ describe('getSliderControllerValue', () => {
59
+ it('returns the configured value', () => {
60
+ expect(
61
+ getSliderControllerValue(
62
+ createSliderController({ thumb: createGuiTestNode(), track: createGuiTestNode(), value: 0.5 }),
63
+ ),
64
+ ).toBe(0.5);
65
+ });
66
+ });
67
+
68
+ describe('setSliderControllerValue', () => {
69
+ it('clamps to the range', () => {
70
+ const controller = createSliderController({ thumb: createGuiTestNode(), track: createGuiTestNode() });
71
+ setSliderControllerValue(controller, 5);
72
+ expect(getSliderControllerValue(controller)).toBe(1);
73
+ });
74
+ });
@@ -0,0 +1,85 @@
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+
3
+ import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
4
+ import {
5
+ createSplitPaneController,
6
+ disposeSplitPaneController,
7
+ getSplitPaneControllerPosition,
8
+ getSplitPaneControllerSignals,
9
+ setSplitPaneControllerPosition,
10
+ } from './splitPaneController';
11
+
12
+ describe('createSplitPaneController', () => {
13
+ it('drags a distinct constrained divider behavior', () => {
14
+ const first = createGuiTestNode(100, 20);
15
+ const divider = createGuiTestNode(10, 20);
16
+ const second = createGuiTestNode(100, 20);
17
+ const controller = createSplitPaneController({
18
+ divider,
19
+ firstRegion: first,
20
+ minimumFirst: 50,
21
+ minimumSecond: 50,
22
+ secondRegion: second,
23
+ totalSize: 210,
24
+ });
25
+ emitGuiPointer(divider, 'onPointerDown', { worldX: 100 });
26
+ emitGuiPointer(divider, 'onPointerMove', { worldX: 170 });
27
+ expect(getSplitPaneControllerPosition(controller)).toBe(150);
28
+ expect(divider.x).toBe(150);
29
+ });
30
+ });
31
+
32
+ describe('disposeSplitPaneController', () => {
33
+ it('detaches divider dragging', () => {
34
+ const divider = createGuiTestNode();
35
+ const controller = createSplitPaneController({
36
+ divider,
37
+ firstRegion: createGuiTestNode(),
38
+ secondRegion: createGuiTestNode(),
39
+ });
40
+ disposeSplitPaneController(controller);
41
+ emitGuiPointer(divider, 'onPointerDown', { worldX: 0 });
42
+ emitGuiPointer(divider, 'onPointerMove', { worldX: 50 });
43
+ expect(getSplitPaneControllerPosition(controller)).toBe(100);
44
+ });
45
+ });
46
+
47
+ describe('getSplitPaneControllerPosition', () => {
48
+ it('returns the clamped initial position', () => {
49
+ const controller = createSplitPaneController({
50
+ divider: createGuiTestNode(),
51
+ firstRegion: createGuiTestNode(),
52
+ minimumFirst: 20,
53
+ position: 0,
54
+ secondRegion: createGuiTestNode(),
55
+ });
56
+ expect(getSplitPaneControllerPosition(controller)).toBe(20);
57
+ });
58
+ });
59
+
60
+ describe('getSplitPaneControllerSignals', () => {
61
+ it('emits changed positions', () => {
62
+ const controller = createSplitPaneController({
63
+ divider: createGuiTestNode(),
64
+ firstRegion: createGuiTestNode(),
65
+ secondRegion: createGuiTestNode(),
66
+ });
67
+ const values: number[] = [];
68
+ connectSignal(getSplitPaneControllerSignals(controller).onChange, (value) => values.push(value));
69
+ setSplitPaneControllerPosition(controller, 50);
70
+ expect(values).toEqual([50]);
71
+ });
72
+ });
73
+
74
+ describe('setSplitPaneControllerPosition', () => {
75
+ it('honors the maximum', () => {
76
+ const controller = createSplitPaneController({
77
+ divider: createGuiTestNode(),
78
+ firstRegion: createGuiTestNode(),
79
+ maximumFirst: 60,
80
+ secondRegion: createGuiTestNode(),
81
+ });
82
+ setSplitPaneControllerPosition(controller, 90);
83
+ expect(getSplitPaneControllerPosition(controller)).toBe(60);
84
+ });
85
+ });
@@ -0,0 +1,50 @@
1
+ import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
2
+ import {
3
+ createTabBarController,
4
+ disposeTabBarController,
5
+ getTabBarControllerSelectedIndex,
6
+ getTabBarControllerSignals,
7
+ setTabBarControllerSelectedIndex,
8
+ } from './tabBarController';
9
+
10
+ describe('createTabBarController', () => {
11
+ it('swaps selected and unselected visuals from clicks', () => {
12
+ const a = { selectedState: createGuiTestNode(), unselectedState: createGuiTestNode() };
13
+ const b = { selectedState: createGuiTestNode(), unselectedState: createGuiTestNode() };
14
+ const controller = createTabBarController({ tabs: [a, b] });
15
+ emitGuiPointer(b.unselectedState, 'onClick');
16
+ expect(getTabBarControllerSelectedIndex(controller)).toBe(1);
17
+ expect([b.selectedState.visible, b.unselectedState.visible]).toEqual([true, false]);
18
+ });
19
+ });
20
+
21
+ describe('disposeTabBarController', () => {
22
+ it('detaches tab listeners', () => {
23
+ const tab = { selectedState: createGuiTestNode(), unselectedState: createGuiTestNode() };
24
+ const controller = createTabBarController({ selectedIndex: -1, tabs: [tab] });
25
+ disposeTabBarController(controller);
26
+ emitGuiPointer(tab.unselectedState, 'onClick');
27
+ expect(getTabBarControllerSelectedIndex(controller)).toBe(-1);
28
+ });
29
+ });
30
+
31
+ describe('getTabBarControllerSelectedIndex', () => {
32
+ it('reports the initial index', () => {
33
+ expect(getTabBarControllerSelectedIndex(createTabBarController({ selectedIndex: -1, tabs: [] }))).toBe(-1);
34
+ });
35
+ });
36
+
37
+ describe('getTabBarControllerSignals', () => {
38
+ it('returns stable signals', () => {
39
+ const controller = createTabBarController({ tabs: [] });
40
+ expect(getTabBarControllerSignals(controller)).toBe(getTabBarControllerSignals(controller));
41
+ });
42
+ });
43
+
44
+ describe('setTabBarControllerSelectedIndex', () => {
45
+ it('normalizes invalid indices', () => {
46
+ const controller = createTabBarController({ tabs: [] });
47
+ setTabBarControllerSelectedIndex(controller, 2);
48
+ expect(getTabBarControllerSelectedIndex(controller)).toBe(-1);
49
+ });
50
+ });
@@ -0,0 +1,93 @@
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+ import { createRichText } from '@flighthq/text/contract';
3
+
4
+ import { createGuiTestNode } from './guiTestHelper';
5
+ import {
6
+ blurTextInputController,
7
+ createTextInputController,
8
+ dispatchTextInputControllerKeyDown,
9
+ dispatchTextInputControllerText,
10
+ disposeTextInputController,
11
+ focusTextInputController,
12
+ getTextInputControllerSignals,
13
+ } from './textInputController';
14
+
15
+ describe('blurTextInputController', () => {
16
+ it('hides a caller caret', () => {
17
+ const caret = createGuiTestNode();
18
+ const controller = createTextInputController({ caret, textField: createRichText() });
19
+ focusTextInputController(controller);
20
+ blurTextInputController(controller);
21
+ expect(caret.visible).toBe(false);
22
+ });
23
+ });
24
+
25
+ describe('createTextInputController', () => {
26
+ it('wraps textinput and relays text changes', () => {
27
+ const field = createRichText();
28
+ const controller = createTextInputController({ textField: field });
29
+ const values: string[] = [];
30
+ connectSignal(getTextInputControllerSignals(controller).onChange, (text) => values.push(text));
31
+ focusTextInputController(controller);
32
+ dispatchTextInputControllerText(controller, 'abc');
33
+ expect(field.data.text).toBe('abc');
34
+ expect(values).toEqual(['abc']);
35
+ });
36
+ });
37
+
38
+ describe('dispatchTextInputControllerKeyDown', () => {
39
+ it('emits submit on Enter', () => {
40
+ const controller = createTextInputController({ textField: createRichText() });
41
+ let submitted = '';
42
+ connectSignal(getTextInputControllerSignals(controller).onSubmit, (text) => (submitted = text));
43
+ focusTextInputController(controller);
44
+ dispatchTextInputControllerText(controller, 'abc');
45
+ dispatchTextInputControllerKeyDown(controller, {
46
+ altKey: false,
47
+ capsLock: false,
48
+ code: 'Enter',
49
+ ctrlKey: false,
50
+ key: 'Enter',
51
+ keyCode: 13,
52
+ location: 0,
53
+ metaKey: false,
54
+ modifier: 0,
55
+ numLock: false,
56
+ repeat: false,
57
+ shiftKey: false,
58
+ timeStamp: 0,
59
+ });
60
+ expect(submitted).toBe('abc');
61
+ });
62
+ });
63
+
64
+ describe('dispatchTextInputControllerText', () => {
65
+ it('returns false while unfocused', () => {
66
+ const controller = createTextInputController({ textField: createRichText() });
67
+ expect(dispatchTextInputControllerText(controller, 'x')).toBe(false);
68
+ });
69
+ });
70
+
71
+ describe('disposeTextInputController', () => {
72
+ it('releases owned text-input capability', () => {
73
+ const controller = createTextInputController({ textField: createRichText() });
74
+ disposeTextInputController(controller);
75
+ expect(dispatchTextInputControllerText(controller, 'x')).toBe(false);
76
+ });
77
+ });
78
+
79
+ describe('focusTextInputController', () => {
80
+ it('shows a caller caret', () => {
81
+ const caret = createGuiTestNode();
82
+ const controller = createTextInputController({ caret, textField: createRichText() });
83
+ focusTextInputController(controller);
84
+ expect(caret.visible).toBe(true);
85
+ });
86
+ });
87
+
88
+ describe('getTextInputControllerSignals', () => {
89
+ it('returns a stable group', () => {
90
+ const controller = createTextInputController({ textField: createRichText() });
91
+ expect(getTextInputControllerSignals(controller)).toBe(getTextInputControllerSignals(controller));
92
+ });
93
+ });
@@ -0,0 +1,72 @@
1
+ import { connectSignal } from '@flighthq/signals/contract';
2
+
3
+ import { createGuiTestNode, emitGuiPointer } from './guiTestHelper';
4
+ import {
5
+ createToggleController,
6
+ disposeToggleController,
7
+ getToggleControllerSignals,
8
+ isToggleControllerChecked,
9
+ setToggleControllerChecked,
10
+ } from './toggleController';
11
+
12
+ describe('createToggleController', () => {
13
+ it('toggles through either state visual and an optional label', () => {
14
+ const unchecked = createGuiTestNode();
15
+ const checked = createGuiTestNode();
16
+ const label = createGuiTestNode();
17
+ const controller = createToggleController({ checkedState: checked, label, uncheckedState: unchecked });
18
+ emitGuiPointer(label, 'onClick');
19
+ expect(isToggleControllerChecked(controller)).toBe(true);
20
+ expect([unchecked.visible, checked.visible]).toEqual([false, true]);
21
+ });
22
+ });
23
+
24
+ describe('disposeToggleController', () => {
25
+ it('detaches click listeners', () => {
26
+ const unchecked = createGuiTestNode();
27
+ const controller = createToggleController({ checkedState: createGuiTestNode(), uncheckedState: unchecked });
28
+ disposeToggleController(controller);
29
+ emitGuiPointer(unchecked, 'onClick');
30
+ expect(isToggleControllerChecked(controller)).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe('getToggleControllerSignals', () => {
35
+ it('emits only real state changes', () => {
36
+ const controller = createToggleController({
37
+ checkedState: createGuiTestNode(),
38
+ uncheckedState: createGuiTestNode(),
39
+ });
40
+ const values: boolean[] = [];
41
+ connectSignal(getToggleControllerSignals(controller).onChange, (value) => values.push(value));
42
+ setToggleControllerChecked(controller, true);
43
+ setToggleControllerChecked(controller, true);
44
+ expect(values).toEqual([true]);
45
+ });
46
+ });
47
+
48
+ describe('isToggleControllerChecked', () => {
49
+ it('reads the current value', () => {
50
+ expect(
51
+ isToggleControllerChecked(
52
+ createToggleController({
53
+ checked: true,
54
+ checkedState: createGuiTestNode(),
55
+ uncheckedState: createGuiTestNode(),
56
+ }),
57
+ ),
58
+ ).toBe(true);
59
+ });
60
+ });
61
+
62
+ describe('setToggleControllerChecked', () => {
63
+ it('sets both directions', () => {
64
+ const controller = createToggleController({
65
+ checked: true,
66
+ checkedState: createGuiTestNode(),
67
+ uncheckedState: createGuiTestNode(),
68
+ });
69
+ setToggleControllerChecked(controller, false);
70
+ expect(isToggleControllerChecked(controller)).toBe(false);
71
+ });
72
+ });