@dcrackel/hematournamentui 1.0.712 → 1.0.714

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 (107) hide show
  1. package/.prettierignore +7 -7
  2. package/.storybook/main.js +3 -1
  3. package/README.md +42 -0
  4. package/dist/HemaTournamentUI-lib.es.js +6856 -6243
  5. package/dist/HemaTournamentUI-lib.umd.js +27 -27
  6. package/dist/hematournamentui-tailwind.css +79 -9
  7. package/package.json +96 -96
  8. package/prettier.config.js +16 -16
  9. package/src/index.js +43 -42
  10. package/src/main.js +1 -1
  11. package/src/mocks/getDEBouts-TableOf128.js +37 -37
  12. package/src/mocks/getDEBouts-TableOf64.js +35 -35
  13. package/src/stories/Atoms/Input/BaseInput.test.js +97 -97
  14. package/src/stories/Atoms/Input/BaseInput.vue +149 -149
  15. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.stories.js +23 -23
  16. package/src/stories/Molecules/Indicators/MissingHemaRatingsId/MissingHemaRatingsId.vue +35 -35
  17. package/src/stories/Molecules/Indicators/RegistrationStatus/RegistrationStatus.vue +30 -30
  18. package/src/stories/Molecules/Menus/HoverActions/HoverActions.test.js +138 -0
  19. package/src/stories/Molecules/Menus/HoverActions/HoverActions.vue +212 -0
  20. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.stories.js +49 -0
  21. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.test.js +129 -0
  22. package/src/stories/Molecules/Modals/BracketDirectorsModal/BracketDirectorsModal.vue +165 -0
  23. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.stories.js +6 -0
  24. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.test.js +18 -18
  25. package/src/stories/Molecules/Modals/DirectorModal/DirectorModal.vue +1 -1
  26. package/src/stories/Organisms/Cards/BoutCard/BoutCard.vue +5 -1
  27. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardActive/BoutCardActive.vue +19 -10
  28. package/src/stories/Organisms/Cards/BoutCard/Variants/BoutCardInactive/BoutCardInactive.vue +11 -7
  29. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.stories.js +42 -33
  30. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.test.js +76 -45
  31. package/src/stories/Organisms/Cards/BracketDirectorAssignmentCard/BracketDirectorAssignmentCard.vue +122 -103
  32. package/src/stories/Organisms/Cards/Director/DirectorCard.test.js +82 -0
  33. package/src/stories/Organisms/Cards/Director/DirectorCard.vue +32 -51
  34. package/src/stories/Organisms/Cards/EditBout/EditBout.test.js +51 -51
  35. package/src/stories/Organisms/Cards/EventCard/EventCard.test.js +106 -106
  36. package/src/stories/Organisms/Cards/FencerCard/FencerCard.vue +298 -298
  37. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.stories.js +77 -60
  38. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.test.js +144 -143
  39. package/src/stories/Organisms/Cards/TableauBoutCard/TableauBoutCard.vue +289 -256
  40. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.stories.js +69 -69
  41. package/src/stories/Organisms/Cards/TableauFencerCard/TableauFencerCard.vue +80 -80
  42. package/src/stories/Organisms/Cards/WaitlistFencerCard/WaitlistFencerCard.vue +153 -153
  43. package/src/stories/Organisms/ComplexInputs/DropDown/DropDownMenu.vue +4 -0
  44. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -31
  45. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -109
  46. package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -192
  47. package/src/stories/Organisms/Containers/Tableau/Tableau.stories.js +1 -0
  48. package/src/stories/Organisms/Containers/Tableau/Tableau.test.js +247 -204
  49. package/src/stories/Organisms/Containers/Tableau/Tableau.vue +89 -15
  50. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.test.js +51 -29
  51. package/src/stories/Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue +63 -46
  52. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.test.js +21 -21
  53. package/src/stories/Organisms/Containers/Tableau/components/TableauSvgOverlay.vue +39 -39
  54. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.js +145 -145
  55. package/src/stories/Organisms/Containers/Tableau/layout/TableauLayoutEngine.test.js +64 -64
  56. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.js +36 -57
  57. package/src/stories/Organisms/Containers/Tableau/plannedAssignments.test.js +101 -65
  58. package/src/stories/Organisms/Containers/TableauColumn/TableauColumn.vue +14 -1
  59. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +52 -52
  60. package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +10 -3
  61. package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -226
  62. package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
  63. package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +137 -137
  64. package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -172
  65. package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -216
  66. package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -79
  67. package/src/stories/Organisms/Wizards/EditEventWizard/RuleSerialization.test.js +29 -29
  68. package/src/stories/Organisms/Wizards/EditEventWizard/WeaponSelection.test.js +138 -0
  69. package/src/stories/Templates/EventManagement/Bracket/Bracket.stories.js +149 -9
  70. package/src/stories/Templates/EventManagement/Bracket/Bracket.test.js +54 -2
  71. package/src/stories/Templates/EventManagement/Bracket/Bracket.vue +77 -8
  72. package/src/stories/Templates/EventManagement/Bracket/BracketDirectorAssignment.test.js +242 -0
  73. package/src/stories/Templates/EventManagement/Bracket/BracketMobile.test.js +131 -0
  74. package/src/stories/Templates/EventManagement/Bracket/BracketPlannedSuggestions.js +50 -39
  75. package/src/stories/Templates/EventManagement/Bracket/BracketStories.test.js +201 -0
  76. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.js +93 -0
  77. package/src/stories/Templates/EventManagement/Bracket/BracketStoryAssignments.test.js +152 -0
  78. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.js +39 -39
  79. package/src/stories/Templates/EventManagement/Bracket/BracketStoryHarness.test.js +79 -79
  80. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -93
  81. package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -91
  82. package/src/stories/Templates/EventManagement/PoolLive/PoolLive.test.js +121 -121
  83. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.stories.js +9 -2
  84. package/src/stories/Templates/Menus/EditEventsTopMenu/EditEventsTopMenu.vue +5 -4
  85. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +81 -81
  86. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -39
  87. package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +101 -101
  88. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.stories.js +71 -71
  89. package/src/stories/Templates/PersonManagement/Admin/FencerAdmin/FencerAdmin.vue +86 -86
  90. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.stories.js +86 -86
  91. package/src/stories/Templates/PersonManagement/Admin/MergeAccounts/MergeAccounts.vue +247 -247
  92. package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -170
  93. package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -156
  94. package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -329
  95. package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -329
  96. package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -42
  97. package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -84
  98. package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +114 -114
  99. package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -363
  100. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -116
  101. package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +307 -307
  102. package/src/stories/Util/Validations/PersonManagementValidations.js +45 -45
  103. package/src/stories/Util/Validations/PriceValidation.js +6 -6
  104. package/src/stories/Util/boutDirectorAssignment.js +46 -0
  105. package/src/stories/Util/bracketDirectors.js +86 -0
  106. package/src/stories/Util/registrationStatus.js +11 -11
  107. package/src/stories/Util/tabs.js +19 -47
@@ -0,0 +1,242 @@
1
+ import { DOMWrapper, mount, shallowMount } from '@vue/test-utils';
2
+ import { nextTick } from 'vue';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+ import Tableau from '../../../Organisms/Containers/Tableau/Tableau.vue';
5
+ import Bracket from './Bracket.vue';
6
+
7
+ const createBout = (overrides = {}) => ({
8
+ BoutId: 42,
9
+ DEBoutId: 7,
10
+ RoundLabel: 'Semifinal',
11
+ Status: 'Scheduled',
12
+ Person1: { PersonId: 1, DisplayName: 'Fencer One' },
13
+ Person2: { PersonId: 2, DisplayName: 'Fencer Two' },
14
+ ...overrides
15
+ });
16
+
17
+ const bracketProps = bout => ({
18
+ bouts: [bout],
19
+ eventRules: [],
20
+ hostingClubColors: {},
21
+ connectedToServer: true,
22
+ showBoutDirectorAssignment: true
23
+ });
24
+
25
+ describe('Bracket assignment request contract', () => {
26
+ it.each(['Tableau', 'List View'])('renders %s without the removed assignment icon', async view => {
27
+ const wrapper = mount(Bracket, {
28
+ props: bracketProps(createBout()),
29
+ global: { stubs: { DirectorModal: true, EditBoutModal: true } }
30
+ });
31
+ try {
32
+ wrapper.vm.handleViewChange(view);
33
+ await wrapper.vm.$nextTick();
34
+ expect(wrapper.find('[data-testid="assign-bout-director"]').exists()).toBe(false);
35
+ expect(wrapper.find('.fa-user-plus').exists()).toBe(false);
36
+ } finally {
37
+ wrapper.unmount();
38
+ }
39
+ });
40
+
41
+ it('keeps forwarded requests detached and leaves bout state unchanged', () => {
42
+ const bout = createBout();
43
+ const wrapper = shallowMount(Bracket, { props: bracketProps(bout) });
44
+ try {
45
+ wrapper.findComponent(Tableau).vm.$emit('request:bout-director', bout);
46
+ expect(wrapper.emitted('request:bout-director')).toEqual([[bout]]);
47
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
48
+ expect(wrapper.emitted('update:bout')).toBeUndefined();
49
+ wrapper.emitted('request:bout-director')[0][0].Person1.DisplayName = 'Changed';
50
+ expect(bout.Person1.DisplayName).toBe('Fencer One');
51
+ expect(bout.Status).toBe('Scheduled');
52
+ } finally {
53
+ wrapper.unmount();
54
+ }
55
+ });
56
+
57
+ it.each(['Active', 'Completed'])('rejects forwarded requests after parent status becomes %s', async status => {
58
+ const bout = createBout();
59
+ const wrapper = shallowMount(Bracket, { props: bracketProps(bout) });
60
+ try {
61
+ await wrapper.setProps({ bouts: [{ ...bout, Status: status }] });
62
+ wrapper.findComponent(Tableau).vm.$emit('request:bout-director', bout);
63
+ expect(wrapper.emitted('request:bout-director')).toBeUndefined();
64
+ } finally {
65
+ wrapper.unmount();
66
+ }
67
+ });
68
+ });
69
+
70
+ const mountedBrackets = [];
71
+ const mountHoverBracket = async (view, bout, overrides = {}) => {
72
+ const wrapper = mount(Bracket, {
73
+ attachTo: document.body,
74
+ props: { ...bracketProps(bout), ...overrides },
75
+ global: { stubs: { DirectorModal: true, EditBoutModal: true } }
76
+ });
77
+ mountedBrackets.push(wrapper);
78
+ wrapper.vm.handleViewChange(view);
79
+ await nextTick();
80
+ return wrapper;
81
+ };
82
+ const revealBoutActions = async wrapper => {
83
+ await wrapper.get('[data-testid="bout-actions-trigger"]').trigger('mouseenter');
84
+ vi.advanceTimersByTime(200);
85
+ await nextTick();
86
+ await nextTick();
87
+ return new DOMWrapper(document.querySelector('[data-testid="bout-actions-menu"]'));
88
+ };
89
+
90
+ describe.each(['Tableau', 'List View'])('%s hover bout actions', view => {
91
+ beforeEach(() => vi.useFakeTimers());
92
+ afterEach(() => {
93
+ mountedBrackets.splice(0).forEach(wrapper => wrapper.unmount());
94
+ vi.useRealTimers();
95
+ });
96
+
97
+ it('shows the assigned director above Assign Director and reconciles reassignment while open', async () => {
98
+ const bout = createBout({ RefName: 'Unassigned' });
99
+ const station = {
100
+ RingId: 7,
101
+ RingName: 'Ring 7',
102
+ Color: 'red',
103
+ Director: { PersonId: 99, DisplayName: 'Dave Smith' }
104
+ };
105
+ const wrapper = await mountHoverBracket(view, bout, {
106
+ plannedDirectingStations: [station],
107
+ plannedBoutAssignments: [{ DEBoutId: bout.DEBoutId, RingId: 7 }]
108
+ });
109
+ const panel = await revealBoutActions(wrapper);
110
+ const action = panel.get('[data-action="assign-director"]');
111
+ const description = panel.get('[data-testid="action-description"]');
112
+ expect(description.text()).toBe('Dave Smith');
113
+ expect(description.element.style.color).toBe('red');
114
+ expect(description.element.closest('button')).toBeNull();
115
+ expect(action.text()).toBe('Assign Director');
116
+ await description.trigger('click');
117
+ expect(wrapper.emitted('request:bout-director')).toBeUndefined();
118
+ expect(panel.isVisible()).toBe(true);
119
+ await wrapper.setProps({
120
+ plannedDirectingStations: [
121
+ { ...station, Color: 'green', Director: { PersonId: 100, DisplayName: 'Alex Morgan' } }
122
+ ]
123
+ });
124
+ expect(panel.get('[data-testid="action-description"]').text()).toBe('Alex Morgan');
125
+ expect(panel.get('[data-testid="action-description"]').element.style.color).toBe('green');
126
+ await panel.get('[data-action="assign-director"]').trigger('click');
127
+ expect(wrapper.emitted('request:bout-director')).toEqual([[bout]]);
128
+ expect(wrapper.emitted('update:bout')).toBeUndefined();
129
+ expect(bout.RefName).toBe('Unassigned');
130
+ });
131
+
132
+ it('omits the name for an unassigned bout', async () => {
133
+ const wrapper = await mountHoverBracket(view, createBout({ RefName: ' Unassigned ' }));
134
+ const panel = await revealBoutActions(wrapper);
135
+ expect(panel.find('[data-testid="action-description"]').exists()).toBe(false);
136
+ expect(panel.get('[data-action="assign-director"]').text()).toBe('Assign Director');
137
+ });
138
+
139
+ it('shows the saved director for an active bout instead of an earlier planned director', async () => {
140
+ const bout = createBout({ Status: 'Active', RefereeId: 99, RefName: 'Dave Smith' });
141
+ const wrapper = await mountHoverBracket(view, bout, {
142
+ plannedDirectingStations: [{ RingId: 7, Color: 'red', Director: { PersonId: 100, DisplayName: 'Alex Morgan' } }],
143
+ plannedBoutAssignments: [{ DEBoutId: bout.DEBoutId, RingId: 7 }]
144
+ });
145
+ const panel = await revealBoutActions(wrapper);
146
+ expect(panel.get('[data-testid="action-description"]').text()).toBe('Dave Smith');
147
+ expect(panel.text()).toContain('Resume bout');
148
+ expect(panel.text()).not.toContain('Alex Morgan');
149
+ expect(panel.get('[data-testid="action-description"]').element.style.color).toBe('');
150
+ expect(panel.find('[data-action="assign-director"]').exists()).toBe(false);
151
+ });
152
+
153
+ it.each(['Final', 'Grand Final', 'Table of 2'])(
154
+ 'keeps %s claimable without offering director assignment',
155
+ async RoundLabel => {
156
+ const bout = createBout({ RoundLabel });
157
+ const wrapper = await mountHoverBracket(view, bout);
158
+ const panel = await revealBoutActions(wrapper);
159
+ expect(panel.find('[data-action="assign-director"]').exists()).toBe(false);
160
+ expect(panel.get('[data-action="open"]').text()).toBe('Open bout');
161
+ wrapper.findComponent(Tableau).vm.$emit('request:bout-director', bout);
162
+ expect(wrapper.emitted('request:bout-director')).toBeUndefined();
163
+ await panel.get('[data-action="open"]').trigger('click');
164
+ expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
165
+ expect(wrapper.vm.showDirectorModal).toBe(false);
166
+ }
167
+ );
168
+
169
+ it('requests assignment without opening the bout or mutating scores or status', async () => {
170
+ const bout = createBout();
171
+ const wrapper = await mountHoverBracket(view, bout);
172
+ expect(document.querySelector('[data-testid="bout-actions-menu"]')).toBeNull();
173
+ const panel = await revealBoutActions(wrapper);
174
+ expect(panel.text()).toContain('Open bout');
175
+ await panel.get('[data-action="assign-director"]').trigger('click');
176
+ expect(wrapper.emitted('request:bout-director')).toEqual([[bout]]);
177
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
178
+ expect(wrapper.emitted('update:bout')).toBeUndefined();
179
+ expect(wrapper.vm.showDirectorModal).toBe(false);
180
+ expect(bout.Status).toBe('Scheduled');
181
+ expect(bout.RefereeId).toBeUndefined();
182
+ });
183
+
184
+ it('opens the scheduled bout through the normal directing contract', async () => {
185
+ const bout = createBout();
186
+ const wrapper = await mountHoverBracket(view, bout);
187
+ const panel = await revealBoutActions(wrapper);
188
+ await panel.get('[data-action="open"]').trigger('click');
189
+ expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
190
+ expect(wrapper.emitted('request:bout-director')).toBeUndefined();
191
+ expect(wrapper.vm.showDirectorModal).toBe(false);
192
+ await wrapper.setProps({ bouts: [{ ...bout, Status: 'Active' }] });
193
+ expect(wrapper.vm.showDirectorModal).toBe(true);
194
+ });
195
+
196
+ it('allows future pairing assignment while withholding Open bout', async () => {
197
+ const bout = createBout({ Person1: { PersonId: 0, DisplayName: 'BYE' }, Person2: null });
198
+ const wrapper = await mountHoverBracket(view, bout);
199
+ const panel = await revealBoutActions(wrapper);
200
+ expect(panel.find('[data-action="open"]').exists()).toBe(false);
201
+ await panel.get('[data-action="assign-director"]').trigger('click');
202
+ expect(wrapper.emitted('request:bout-director')).toEqual([[bout]]);
203
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
204
+ });
205
+
206
+ it('keeps assignment unavailable unless enabled by the parent', async () => {
207
+ const wrapper = await mountHoverBracket(view, createBout(), { showBoutDirectorAssignment: false });
208
+ const panel = await revealBoutActions(wrapper);
209
+ expect(panel.find('[data-action="assign-director"]').exists()).toBe(false);
210
+ expect(panel.get('[data-action="open"]').text()).toBe('Open bout');
211
+ });
212
+
213
+ it('changes the menu to Resume bout when the parent starts a bout', async () => {
214
+ const bout = createBout();
215
+ const wrapper = await mountHoverBracket(view, bout);
216
+ await revealBoutActions(wrapper);
217
+ await wrapper.setProps({ bouts: [{ ...bout, Status: 'Active' }] });
218
+ const panel = new DOMWrapper(document.querySelector('[data-testid="bout-actions-menu"]'));
219
+ expect(panel.get('[data-action="open"]').text()).toBe('Resume bout');
220
+ expect(panel.find('[data-action="assign-director"]').exists()).toBe(false);
221
+ await panel.get('[data-action="open"]').trigger('click');
222
+ expect(wrapper.emitted('update:directing-bout')[0][0].Status).toBe('Active');
223
+ });
224
+ });
225
+
226
+ describe('Completed tableau bout hover actions', () => {
227
+ it('offers Edit bout and opens the existing edit modal', async () => {
228
+ vi.useFakeTimers();
229
+ try {
230
+ const wrapper = await mountHoverBracket('Tableau', createBout({ Status: 'Completed' }));
231
+ const panel = await revealBoutActions(wrapper);
232
+ expect(panel.get('[data-action="open"]').text()).toBe('Edit bout');
233
+ expect(panel.find('[data-action="assign-director"]').exists()).toBe(false);
234
+ await panel.get('[data-action="open"]').trigger('click');
235
+ expect(wrapper.vm.showEditBoutModal).toBe(true);
236
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
237
+ } finally {
238
+ mountedBrackets.splice(0).forEach(wrapper => wrapper.unmount());
239
+ vi.useRealTimers();
240
+ }
241
+ });
242
+ });
@@ -0,0 +1,131 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+ import DirectorModal from '../../../Molecules/Modals/DirectorModal/DirectorModal.vue';
4
+ import EditBoutModal from '../../../Molecules/Modals/EditBoutModal/EditBoutModal.vue';
5
+ import Tableau from '../../../Organisms/Containers/Tableau/Tableau.vue';
6
+ import BracketDirectorAssignmentPanel from '../../../Organisms/Containers/Tableau/components/BracketDirectorAssignmentPanel.vue';
7
+ import ToggleHeader from '../../../Organisms/Headers/ToggleHeader/ToggleHeader.vue';
8
+ import EditEventsTopMenu from '../../Menus/EditEventsTopMenu/EditEventsTopMenu.vue';
9
+ import Bracket from './Bracket.vue';
10
+
11
+ const bout = {
12
+ BoutId: 42,
13
+ DEBoutId: 1,
14
+ RoundLabel: 'Final',
15
+ Status: 'Scheduled',
16
+ Score1: 4,
17
+ Score2: 3,
18
+ Person1: { PersonId: 1, DisplayName: 'Fencer One' },
19
+ Person2: { PersonId: 2, DisplayName: 'Fencer Two' }
20
+ };
21
+ const stations = [{ RingId: 7, RingName: 'Ring 7', Director: { PersonId: 99, DisplayName: 'Director' } }];
22
+ const wrappers = [];
23
+ const mountBracket = (width = 390) => {
24
+ vi.stubGlobal('innerWidth', width);
25
+ const wrapper = shallowMount(Bracket, {
26
+ props: {
27
+ bouts: [bout],
28
+ eventRules: [],
29
+ hostingClubColors: {},
30
+ connectedToServer: true,
31
+ role: 'Admin',
32
+ showBoutDirectorAssignment: true,
33
+ showPlannedDirectorAssignments: true,
34
+ plannedDirectingStations: stations,
35
+ plannedBoutAssignments: [{ DEBoutId: 1, RingId: 7 }],
36
+ viewerPersonId: 99
37
+ }
38
+ });
39
+ wrappers.push(wrapper);
40
+ return wrapper;
41
+ };
42
+ const resize = async (wrapper, width) => {
43
+ window.innerWidth = width;
44
+ window.dispatchEvent(new Event('resize'));
45
+ await wrapper.vm.$nextTick();
46
+ };
47
+
48
+ afterEach(() => {
49
+ wrappers.splice(0).forEach(wrapper => wrapper.unmount());
50
+ vi.unstubAllGlobals();
51
+ });
52
+
53
+ describe('Bracket phone directing view', () => {
54
+ it.each([320, 390, 767])('shows a bout list without organising controls at %ipx', width => {
55
+ const wrapper = mountBracket(width);
56
+ expect(wrapper.findComponent(Tableau).props()).toMatchObject({
57
+ listView: true,
58
+ showBoutDirectorAssignment: false,
59
+ plannedDirectingStations: stations,
60
+ viewerPersonId: 99
61
+ });
62
+ expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(false);
63
+ expect(
64
+ wrapper
65
+ .findComponent(EditEventsTopMenu)
66
+ .props('tabs')
67
+ .map(tab => tab.id)
68
+ ).toEqual(['Pools', 'Pool Results', 'Bracket', 'Final', 'Penalties']);
69
+ wrapper.findComponent(Tableau).vm.$emit('request:bout-director', bout);
70
+ expect(wrapper.emitted('request:bout-director')).toBeUndefined();
71
+ });
72
+
73
+ it('preserves the desktop view choice and full navigation across phone resizing', async () => {
74
+ const wrapper = mountBracket(1280);
75
+ expect(wrapper.findComponent(Tableau).props('listView')).toBe(false);
76
+ wrapper.findComponent(ToggleHeader).vm.$emit('update:selection', 'List View');
77
+ await wrapper.vm.$nextTick();
78
+ expect(
79
+ wrapper
80
+ .findComponent(EditEventsTopMenu)
81
+ .props('tabs')
82
+ .map(tab => tab.id)
83
+ ).toContain('Staff');
84
+ expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(true);
85
+ await resize(wrapper, 390);
86
+ expect(wrapper.findComponent(BracketDirectorAssignmentPanel).exists()).toBe(false);
87
+ await resize(wrapper, 768);
88
+ expect(wrapper.findComponent(Tableau).props('listView')).toBe(true);
89
+ expect(wrapper.findComponent(ToggleHeader).props('currentSelection')).toBe('List View');
90
+ expect(wrapper.findComponent(Tableau).props('showBoutDirectorAssignment')).toBe(true);
91
+ wrapper.findComponent(ToggleHeader).vm.$emit('update:selection', 'Tableau');
92
+ await wrapper.vm.$nextTick();
93
+ await resize(wrapper, 390);
94
+ expect(wrapper.findComponent(Tableau).props('listView')).toBe(true);
95
+ await resize(wrapper, 1280);
96
+ expect(wrapper.findComponent(Tableau).props('listView')).toBe(false);
97
+ expect(wrapper.findComponent(ToggleHeader).props('currentSelection')).toBe('Tableau');
98
+ });
99
+
100
+ it('keeps scheduled start, active resume, and modal close parent-owned on phones', async () => {
101
+ const wrapper = mountBracket();
102
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', bout);
103
+ await wrapper.vm.$nextTick();
104
+ expect(wrapper.emitted('update:directing-bout')).toEqual([[bout]]);
105
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(false);
106
+ const activeBout = { ...bout, Status: 'Active' };
107
+ await wrapper.setProps({ bouts: [activeBout] });
108
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(true);
109
+ wrapper.findComponent(DirectorModal).vm.$emit('update:closeModal');
110
+ await wrapper.vm.$nextTick();
111
+ expect(wrapper.emitted('close:directing-bout')).toEqual([[activeBout]]);
112
+ expect(wrapper.emitted('update:bout')).toBeUndefined();
113
+ expect(wrapper.vm.boutsData[0]).toEqual(activeBout);
114
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', activeBout);
115
+ await wrapper.vm.$nextTick();
116
+ expect(wrapper.findComponent(DirectorModal).props('show')).toBe(true);
117
+ });
118
+
119
+ it('retains completed editing and rejects incomplete bouts on phones', async () => {
120
+ const wrapper = mountBracket();
121
+ const completedBout = { ...bout, Status: 'Completed' };
122
+ await wrapper.setProps({ bouts: [completedBout] });
123
+ wrapper.findComponent(Tableau).vm.$emit('action:editBout', completedBout);
124
+ await wrapper.vm.$nextTick();
125
+ expect(wrapper.findComponent(EditBoutModal).props('show')).toBe(true);
126
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
127
+ await wrapper.setProps({ bouts: [{ ...bout, Person2: null }] });
128
+ wrapper.findComponent(Tableau).vm.$emit('action:directingBout', { ...bout, Person2: null });
129
+ expect(wrapper.emitted('update:directing-bout')).toBeUndefined();
130
+ });
131
+ });
@@ -1,39 +1,50 @@
1
- import portraitUrl from '../../../../assets/portrait1.png';
2
-
3
- const createBoutAssignments = (ringId, boutIds) => boutIds.map(DEBoutId => ({ DEBoutId, RingId: ringId }));
4
-
5
- export const plannedDirectingStations = [
6
- {
7
- RingId: 1,
8
- RingName: 'Ring 1',
9
- Color: '#2563eb',
10
- Director: { PersonId: 1001, DisplayName: 'Alex Morgan', PortraitUrl: portraitUrl }
11
- },
12
- {
13
- RingId: 2,
14
- RingName: 'Ring 2',
15
- Color: '#dc2626',
16
- Director: { PersonId: 1002, DisplayName: 'Jordan Lee', PortraitUrl: portraitUrl }
17
- },
18
- {
19
- RingId: 3,
20
- RingName: 'Ring 3',
21
- Color: '#059669',
22
- Director: { PersonId: 1003, DisplayName: 'Sam Rivera', PortraitUrl: portraitUrl }
23
- },
24
- {
25
- RingId: 4,
26
- RingName: 'Ring 4',
27
- Color: '#d97706',
28
- Director: { PersonId: 1004, DisplayName: 'Taylor Quinn', PortraitUrl: portraitUrl }
29
- }
30
- ];
31
-
32
- export const plannedBoutAssignments = [
33
- ...createBoutAssignments(1, [65, 66, 67, 68, 69, 70, 71, 72, 97, 98, 99, 100, 113, 114, 121, 125]),
34
- ...createBoutAssignments(2, [73, 74, 75, 76, 77, 78, 79, 80, 101, 102, 103, 104, 115, 116, 122]),
35
- ...createBoutAssignments(3, [81, 82, 83, 84, 85, 86, 87, 88, 105, 106, 107, 108, 117, 118, 123, 126]),
36
- ...createBoutAssignments(4, [89, 90, 91, 92, 93, 94, 95, 96, 109, 110, 111, 112, 119, 120, 124, 127])
37
- ];
38
-
39
- export const plannedSuggestionViewerId = plannedDirectingStations[0].Director.PersonId;
1
+ import portraitUrl from '../../../../assets/portrait1.png';
2
+
3
+ export const plannedDirectingStations = [
4
+ {
5
+ RingId: 1,
6
+ RingName: 'Ring 1',
7
+ Color: '#2563eb',
8
+ Director: { PersonId: 1001, DisplayName: 'Alex Morgan', PortraitUrl: portraitUrl }
9
+ },
10
+ {
11
+ RingId: 2,
12
+ RingName: 'Ring 2',
13
+ Color: '#dc2626',
14
+ Director: { PersonId: 1002, DisplayName: 'Jordan Lee', PortraitUrl: portraitUrl }
15
+ },
16
+ {
17
+ RingId: 3,
18
+ RingName: 'Ring 3',
19
+ Color: '#059669',
20
+ Director: { PersonId: 1003, DisplayName: 'Sam Rivera', PortraitUrl: portraitUrl }
21
+ },
22
+ {
23
+ RingId: 4,
24
+ RingName: 'Ring 4',
25
+ Color: '#d97706',
26
+ Director: { PersonId: 1004, DisplayName: 'Taylor Quinn', PortraitUrl: portraitUrl }
27
+ },
28
+ {
29
+ RingId: 5,
30
+ RingName: 'Ring 5',
31
+ Color: '#9333ea',
32
+ Director: { PersonId: 1005, DisplayName: 'Casey Brooks', PortraitUrl: portraitUrl }
33
+ },
34
+ {
35
+ RingId: 6,
36
+ RingName: 'Ring 6',
37
+ Color: '#0891b2',
38
+ Director: { PersonId: 1006, DisplayName: 'Drew Parker', PortraitUrl: portraitUrl }
39
+ }
40
+ ];
41
+
42
+ export const plannedSuggestionViewerId = plannedDirectingStations[0].Director.PersonId;
43
+
44
+ export const availableDirectors = plannedDirectingStations.map((station, index) => ({
45
+ ...station.Director,
46
+ Color: station.Color,
47
+ DefaultRingId: index < 2 ? station.RingId : null
48
+ }));
49
+
50
+ export const availableRings = plannedDirectingStations.map(({ RingId, RingName }) => ({ RingId, RingName }));
@@ -0,0 +1,201 @@
1
+ import { shallowMount } from '@vue/test-utils';
2
+ import { nextTick, reactive } from 'vue';
3
+ import { afterEach, describe, expect, it } from 'vitest';
4
+ import Bracket from './Bracket.vue';
5
+ import meta, * as stories from './Bracket.stories.js';
6
+
7
+ const wrappers = [];
8
+ const mountStory = story => {
9
+ const args = reactive({ ...meta.args, ...story.args });
10
+ const wrapper = shallowMount(meta.render(args));
11
+ wrappers.push(wrapper);
12
+ return { wrapper, args, bracket: () => wrapper.findComponent(Bracket) };
13
+ };
14
+ const getRing = (bracket, bout) =>
15
+ bracket.props('plannedBoutAssignments').find(item => String(item.DEBoutId) === String(bout.DEBoutId))?.RingId;
16
+
17
+ afterEach(() => wrappers.splice(0).forEach(wrapper => wrapper.unmount()));
18
+
19
+ describe('Bracket story assignment simulation', () => {
20
+ it('enables assignment and its panel in every bracket story', () => {
21
+ for (const story of Object.values(stories).filter(value => value !== meta)) {
22
+ const args = { ...meta.args, ...story.args };
23
+ expect(args.showBoutDirectorAssignment).toBe(true);
24
+ expect(args.showPlannedDirectorAssignments).toBe(true);
25
+ expect(args.plannedDirectingStations.length).toBeGreaterThan(0);
26
+ }
27
+ });
28
+
29
+ it('starts with two actual stations and recalculates scheduled coverage when directors are added', async () => {
30
+ const { bracket } = mountStory(stories.PlannedDirectorSuggestions);
31
+ expect(bracket().props('plannedDirectingStations')).toHaveLength(2);
32
+ const bouts = bracket().props('bouts');
33
+ const active = { ...bouts.find(bout => bout.Person1.PersonId > 0 && bout.Person2.PersonId > 0), Status: 'Active' };
34
+ const originalRing = getRing(bracket(), active);
35
+ bracket().vm.$emit('update:bout', active);
36
+ await nextTick();
37
+ for (const count of [3, 4, 5, 6]) {
38
+ bracket().vm.$emit('request:add-planned-director');
39
+ await nextTick();
40
+ const stationIds = bracket()
41
+ .props('plannedDirectingStations')
42
+ .map(station => station.RingId);
43
+ expect(stationIds).toHaveLength(count);
44
+ expect(
45
+ bracket()
46
+ .props('plannedBoutAssignments')
47
+ .every(item => stationIds.includes(item.RingId))
48
+ ).toBe(true);
49
+ expect(getRing(bracket(), active)).toBe(originalRing);
50
+ expect(
51
+ bracket()
52
+ .props('bouts')
53
+ .find(bout => bout.BoutId === active.BoutId).Status
54
+ ).toBe('Active');
55
+ }
56
+ expect(bracket().props('showAddPlannedDirectorCard')).toBe(false);
57
+ });
58
+
59
+ it('recalculates from station controls and assigns semifinals and keeps finals shared for every director count', async () => {
60
+ for (const story of [
61
+ stories.PlannedDirectorSuggestions,
62
+ stories.ThreeDirectors,
63
+ stories.FourDirectors,
64
+ stories.FiveDirectors,
65
+ stories.SixDirectors
66
+ ]) {
67
+ const { bracket, args } = mountStory(story);
68
+ const bouts = bracket().props('bouts');
69
+ const shared = bouts.filter(bout => bout.RoundLabel === 'Final');
70
+ const semifinals = bouts.filter(bout => bout.RoundLabel === 'Semifinal');
71
+ expect(semifinals.every(bout => getRing(bracket(), bout) !== undefined)).toBe(true);
72
+ expect(shared.every(bout => getRing(bracket(), bout) === undefined)).toBe(true);
73
+ args.plannedDirectingStations = stories.SixDirectors.args.plannedDirectingStations;
74
+ await nextTick();
75
+ expect(bracket().props('plannedDirectingStations')).toHaveLength(6);
76
+ expect(semifinals.every(bout => getRing(bracket(), bout) !== undefined)).toBe(true);
77
+ expect(shared.every(bout => getRing(bracket(), bout) === undefined)).toBe(true);
78
+ }
79
+ });
80
+
81
+ it.each(['Active', 'Completed'])(
82
+ 'redistributes scheduled work after removal while preserving a %s bout',
83
+ async Status => {
84
+ const { bracket } = mountStory(stories.PlannedDirectorSuggestions);
85
+ const station = bracket().props('plannedDirectingStations')[0];
86
+ const bout = bracket()
87
+ .props('bouts')
88
+ .find(item => item.Person1.PersonId > 0 && item.Person2.PersonId > 0);
89
+ const savedBout = {
90
+ ...bout,
91
+ Status,
92
+ RefereeId: station.Director.PersonId,
93
+ RefName: station.Director.DisplayName,
94
+ Score1: 4,
95
+ Score2: 3
96
+ };
97
+ bracket().vm.$emit('update:bout', savedBout);
98
+ await nextTick();
99
+ bracket().vm.$emit('request:remove-planned-director', station);
100
+ await nextTick();
101
+ expect(
102
+ bracket()
103
+ .props('plannedDirectingStations')
104
+ .map(item => item.RingId)
105
+ ).toEqual([2]);
106
+ expect(
107
+ bracket()
108
+ .props('bouts')
109
+ .find(item => item.BoutId === bout.BoutId)
110
+ ).toEqual(savedBout);
111
+ const scheduledIds = new Set(
112
+ bracket()
113
+ .props('bouts')
114
+ .filter(item => item.Status === 'Scheduled')
115
+ .map(item => item.DEBoutId)
116
+ );
117
+ expect(
118
+ bracket()
119
+ .props('plannedBoutAssignments')
120
+ .filter(item => scheduledIds.has(item.DEBoutId))
121
+ .every(item => item.RingId === 2)
122
+ ).toBe(true);
123
+ expect(getRing(bracket(), savedBout)).toBe(1);
124
+ }
125
+ );
126
+
127
+ it('supports removing every director and adding a removed director again without changing story args', async () => {
128
+ const { bracket, args } = mountStory(stories.PlannedDirectorSuggestions);
129
+ for (const station of args.plannedDirectingStations) {
130
+ bracket().vm.$emit('request:remove-planned-director', station);
131
+ await nextTick();
132
+ }
133
+ expect(bracket().props('plannedDirectingStations')).toEqual([]);
134
+ expect(bracket().props('plannedBoutAssignments')).toEqual([]);
135
+ expect(bracket().props('showAddPlannedDirectorCard')).toBe(true);
136
+ bracket().vm.$emit('request:add-planned-director');
137
+ await nextTick();
138
+ expect(
139
+ bracket()
140
+ .props('plannedDirectingStations')
141
+ .map(station => station.RingId)
142
+ ).toEqual([1]);
143
+ expect(
144
+ bracket()
145
+ .props('plannedBoutAssignments')
146
+ .every(item => item.RingId === 1)
147
+ ).toBe(true);
148
+ expect(args.plannedDirectingStations).toHaveLength(2);
149
+ });
150
+
151
+ it('keeps the signed-in director identity when resuming a bout after removing their planned station', async () => {
152
+ const { bracket } = mountStory(stories.PlannedDirectorSuggestions);
153
+ const station = bracket().props('plannedDirectingStations')[0];
154
+ const bout = bracket()
155
+ .props('bouts')
156
+ .find(item => item.Person1.PersonId > 0 && item.Person2.PersonId > 0);
157
+ bracket().vm.$emit('update:directing-bout', bout);
158
+ await nextTick();
159
+ const active = bracket()
160
+ .props('bouts')
161
+ .find(item => item.BoutId === bout.BoutId);
162
+ bracket().vm.$emit('request:remove-planned-director', station);
163
+ await nextTick();
164
+ bracket().vm.$emit('update:directing-bout', { ...active, Score1: 3 });
165
+ await nextTick();
166
+ expect(
167
+ bracket()
168
+ .props('bouts')
169
+ .find(item => item.BoutId === bout.BoutId)
170
+ ).toMatchObject({
171
+ RefereeId: station.Director.PersonId,
172
+ RefName: station.Director.DisplayName,
173
+ Status: 'Active',
174
+ Score1: 3
175
+ });
176
+ });
177
+
178
+ it('applies staff-fed roster saves and ring moves only to DE plans', async () => {
179
+ const { bracket, args } = mountStory(stories.PlannedDirectorSuggestions);
180
+ const beforeBouts = JSON.parse(JSON.stringify(bracket().props('bouts')));
181
+ const original = bracket().props('plannedDirectingStations')[0];
182
+ const updated = { ...original, RingId: 3, RingName: 'Ring 3' };
183
+ bracket().vm.$emit('update:planned-directing-stations', [
184
+ updated,
185
+ ...bracket().props('plannedDirectingStations').slice(1)
186
+ ]);
187
+ await nextTick();
188
+ expect(bracket().props('plannedDirectingStations')[0]).toEqual(updated);
189
+ expect(
190
+ bracket()
191
+ .props('plannedBoutAssignments')
192
+ .every(item => [2, 3].includes(item.RingId))
193
+ ).toBe(true);
194
+ expect(args.plannedDirectingStations[0].RingId).toBe(1);
195
+ expect(args.availableDirectors[0].DefaultRingId).toBe(1);
196
+ bracket().vm.$emit('update:planned-directing-stations', [updated]);
197
+ await nextTick();
198
+ expect(bracket().props('plannedDirectingStations')).toEqual([updated]);
199
+ expect(bracket().props('bouts')).toEqual(beforeBouts);
200
+ });
201
+ });