@dcrackel/hematournamentui 1.0.708 → 1.0.710
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 +148 -106
- package/dist/HemaTournamentUI-lib.es.js +6678 -6196
- package/dist/HemaTournamentUI-lib.umd.js +27 -27
- package/dist/hematournamentui-tailwind.css +16 -6
- package/package.json +1 -1
- package/src/stories/Atoms/Input/BaseInput.test.js +97 -85
- package/src/stories/Atoms/Input/BaseInput.vue +149 -150
- package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.stories.js +31 -20
- package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.test.js +109 -0
- package/src/stories/Organisms/ComplexInputs/ImageCropper/ImageCropper.vue +192 -133
- package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.stories.js +48 -41
- package/src/stories/Organisms/Wizards/EditEventWizard/BasicEventInfo/BasicEventInfo.vue +227 -206
- package/src/stories/Organisms/Wizards/EditEventWizard/DirectElimination/DirectElimination.vue +226 -225
- package/src/stories/Organisms/Wizards/EditEventWizard/DirectEliminationPromotion/DirectEliminationPromotion.vue +204 -204
- package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.stories.js +133 -91
- package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.test.js +191 -0
- package/src/stories/Organisms/Wizards/EditEventWizard/EditEventWizard.vue +366 -309
- package/src/stories/Organisms/Wizards/EditEventWizard/EventCustomization/EventCustomization.vue +172 -171
- package/src/stories/Organisms/Wizards/EditEventWizard/PoolConfiguration/PoolConfiguration.vue +216 -215
- package/src/stories/Organisms/Wizards/EditEventWizard/PriceFields.test.js +79 -0
- package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.stories.js +93 -52
- package/src/stories/Templates/EventManagement/EditEventInfo/EditEventInfo.vue +91 -75
- package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.stories.js +14 -0
- package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.test.js +39 -0
- package/src/stories/Templates/PersonManagement/Admin/Administration/Administration.vue +13 -3
- package/src/stories/Templates/PersonManagement/Basic/BasicEdit.vue +170 -172
- package/src/stories/Templates/PersonManagement/PersonManagement.stories.js +156 -103
- package/src/stories/Templates/PersonManagement/PersonManagement.test.js +329 -0
- package/src/stories/Templates/PersonManagement/PersonManagement.vue +329 -213
- package/src/stories/Templates/PersonManagement/PersonManagementDraft.js +42 -0
- package/src/stories/Templates/TournamentManagement/TournamentManagement.stories.js +84 -83
- package/src/stories/Templates/TournamentManagement/TournamentManagement.test.js +89 -0
- package/src/stories/Templates/TournamentManagement/TournamentManagement.vue +363 -360
- package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.stories.js +116 -113
- package/src/stories/Templates/TournamentManagement/TournamentPageOne/TournamentPageOne.vue +292 -280
- package/src/stories/Util/Validations/PersonManagementValidations.js +45 -29
- package/src/stories/Util/Validations/PriceValidation.js +6 -0
- package/src/stories/Util/Validations/TournamentFieldsValidations.js +4 -0
|
@@ -1,103 +1,156 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
1
|
+
import { ref } from 'vue';
|
|
2
|
+
import PersonManagement from './PersonManagement.vue';
|
|
3
|
+
import clubGetAllMock from '../../../mocks/clubGetAllMock.js';
|
|
4
|
+
import personsMock from '../../../mocks/personsMock.js';
|
|
5
|
+
import attendance from '../../../mocks/getAttendance.js';
|
|
6
|
+
import allPersonsMock from '../../../mocks/personGetAllMock.js';
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
title: 'Templates/PersonManagement/PersonManagement',
|
|
10
|
+
component: PersonManagement,
|
|
11
|
+
tags: ['autodocs'],
|
|
12
|
+
args: {
|
|
13
|
+
personProfile: personsMock[1],
|
|
14
|
+
fencingClubs: clubGetAllMock,
|
|
15
|
+
attendance: attendance,
|
|
16
|
+
uploadImageName: 'profile-test',
|
|
17
|
+
uploadServer: 'http://localhost:3000/api/upload/',
|
|
18
|
+
imageServer: 'https://meyersquaredimages.com/uploads/',
|
|
19
|
+
userLevel: 'User',
|
|
20
|
+
saving: false,
|
|
21
|
+
disabled: false,
|
|
22
|
+
draftResetKey: 0
|
|
23
|
+
},
|
|
24
|
+
argTypes: {
|
|
25
|
+
personProfile: { control: { type: 'object' } },
|
|
26
|
+
fencingClubs: { control: { type: 'object' } },
|
|
27
|
+
attendance: { control: { type: 'object' } },
|
|
28
|
+
uploadImageName: { control: { type: 'text' } },
|
|
29
|
+
uploadServer: { control: { type: 'text' } },
|
|
30
|
+
imageServer: { control: { type: 'text' } },
|
|
31
|
+
userLevel: { control: { type: 'select' }, options: ['User', 'Club', 'Admin'] },
|
|
32
|
+
saving: { control: 'boolean', description: 'Disables editing and actions while the parent saves.' },
|
|
33
|
+
disabled: { control: 'boolean', description: 'Disables the entire profile form.' },
|
|
34
|
+
draftResetKey: { control: 'number', description: 'Change after a confirmed reload to replace the draft.' },
|
|
35
|
+
'submit:person': { action: 'submit:person' },
|
|
36
|
+
'update:image': { action: 'update:image' },
|
|
37
|
+
'error:image': { action: 'error:image' }
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const Default = {
|
|
42
|
+
args: {
|
|
43
|
+
personProfile: personsMock[1],
|
|
44
|
+
fencingClubs: clubGetAllMock,
|
|
45
|
+
attendance: attendance,
|
|
46
|
+
uploadImageName: 'profile-test',
|
|
47
|
+
uploadServer: 'http://localhost:3000/api/upload/',
|
|
48
|
+
imageServer: 'http://localhost:3000/uploads/',
|
|
49
|
+
userLevel: 'User'
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const admin = {
|
|
54
|
+
args: {
|
|
55
|
+
personProfile: personsMock[1],
|
|
56
|
+
fencingClubs: clubGetAllMock,
|
|
57
|
+
attendance: attendance,
|
|
58
|
+
people: allPersonsMock,
|
|
59
|
+
uploadImageName: 'profile-test',
|
|
60
|
+
uploadServer: 'http://localhost:3000/api/upload/',
|
|
61
|
+
imageServer: 'http://localhost:3000/uploads/',
|
|
62
|
+
userLevel: 'Admin'
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const clubAdmin = {
|
|
67
|
+
args: {
|
|
68
|
+
personProfile: personsMock[1],
|
|
69
|
+
fencingClubs: clubGetAllMock,
|
|
70
|
+
people: allPersonsMock,
|
|
71
|
+
attendance: attendance,
|
|
72
|
+
uploadImageName: 'profile-test',
|
|
73
|
+
uploadServer: 'http://localhost:3000/api/upload/',
|
|
74
|
+
imageServer: 'http://localhost:3000/uploads/',
|
|
75
|
+
userLevel: 'Club'
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const adminWithLoading = {
|
|
80
|
+
args: {
|
|
81
|
+
personProfile: personsMock[1],
|
|
82
|
+
fencingClubs: clubGetAllMock,
|
|
83
|
+
attendance: attendance,
|
|
84
|
+
people: allPersonsMock,
|
|
85
|
+
uploadImageName: 'profile-test',
|
|
86
|
+
uploadServer: 'http://localhost:3000/api/upload/',
|
|
87
|
+
imageServer: 'http://localhost:3000/uploads/',
|
|
88
|
+
userLevel: 'Admin',
|
|
89
|
+
loading: true
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export const Saving = {
|
|
94
|
+
args: { ...admin.args, saving: true }
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const Disabled = {
|
|
98
|
+
args: { ...Default.args, disabled: true }
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const CurrentDraft = {
|
|
102
|
+
args: {
|
|
103
|
+
...admin.args,
|
|
104
|
+
personProfile: {
|
|
105
|
+
...personsMock[1],
|
|
106
|
+
LegalName: 'Updated Legal Name',
|
|
107
|
+
EmailAddress: 'draft@example.com',
|
|
108
|
+
HemaRatingsId: null,
|
|
109
|
+
Images: []
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const ResetDraft = {
|
|
115
|
+
args: { ...admin.args },
|
|
116
|
+
render: args => ({
|
|
117
|
+
components: { PersonManagement },
|
|
118
|
+
setup() {
|
|
119
|
+
const revision = ref(0);
|
|
120
|
+
const resetDraft = () => {
|
|
121
|
+
revision.value += 1;
|
|
122
|
+
};
|
|
123
|
+
return { args, revision, resetDraft };
|
|
124
|
+
},
|
|
125
|
+
template: `
|
|
126
|
+
<div>
|
|
127
|
+
<button type="button" class="mb-4 rounded border border-dropdownSelect p-2 text-quinary" @click="resetDraft">
|
|
128
|
+
Reset unsaved changes to supplied profile
|
|
129
|
+
</button>
|
|
130
|
+
<PersonManagement v-bind="args" :draftResetKey="revision" />
|
|
131
|
+
</div>
|
|
132
|
+
`
|
|
133
|
+
})
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export const emptyProfile = {
|
|
137
|
+
args: {
|
|
138
|
+
personProfile: {
|
|
139
|
+
DisplayName: '',
|
|
140
|
+
LegalName: '',
|
|
141
|
+
EmailAddress: '',
|
|
142
|
+
Club: { Name: 'No Club Affiliation ', ClubId: '17' },
|
|
143
|
+
AccessLevel: 'User',
|
|
144
|
+
ShareData: true,
|
|
145
|
+
ShowData: true
|
|
146
|
+
},
|
|
147
|
+
fencingClubs: clubGetAllMock,
|
|
148
|
+
attendance: attendance,
|
|
149
|
+
people: allPersonsMock,
|
|
150
|
+
uploadImageName: 'profile-test',
|
|
151
|
+
uploadServer: 'http://localhost:3000/api/upload/',
|
|
152
|
+
imageServer: 'http://localhost:3000/uploads/',
|
|
153
|
+
userLevel: 'Admin',
|
|
154
|
+
loading: false
|
|
155
|
+
}
|
|
156
|
+
};
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
2
|
+
import { enableAutoUnmount, mount } from '@vue/test-utils';
|
|
3
|
+
import { nextTick } from 'vue';
|
|
4
|
+
import PersonManagement from './PersonManagement.vue';
|
|
5
|
+
import BasicEdit from './Basic/BasicEdit.vue';
|
|
6
|
+
import Administration from './Admin/Administration/Administration.vue';
|
|
7
|
+
import TitledInput from '../../Molecules/CombinationInputs/TitledInput/TitledInput.vue';
|
|
8
|
+
import BaseButton from '../../Molecules/Buttons/BaseButton/BaseButton.vue';
|
|
9
|
+
import ImageCropper from '../../Organisms/ComplexInputs/ImageCropper/ImageCropper.vue';
|
|
10
|
+
import EditEventsTopMenu from '../Menus/EditEventsTopMenu/EditEventsTopMenu.vue';
|
|
11
|
+
|
|
12
|
+
enableAutoUnmount(afterEach);
|
|
13
|
+
|
|
14
|
+
const createProfile = overrides => ({
|
|
15
|
+
PersonId: 1,
|
|
16
|
+
DisplayName: 'Original Fencer',
|
|
17
|
+
FirstName: 'Original',
|
|
18
|
+
LastName: 'Name',
|
|
19
|
+
Images: [],
|
|
20
|
+
Club: { Name: 'Example Club', ClubId: 17 },
|
|
21
|
+
AccessLevel: 'User',
|
|
22
|
+
HemaRatingsId: 123,
|
|
23
|
+
HEMARatings: [{ HemaRatingId: 123, Rating: 1600 }],
|
|
24
|
+
PersonEmails: [{ IsPrimary: true, EmailAddress: 'original@example.com' }],
|
|
25
|
+
...overrides
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const mountProfile = (personProfile = createProfile(), props = {}) =>
|
|
29
|
+
mount(PersonManagement, {
|
|
30
|
+
props: { personProfile, userLevel: 'Admin', ...props },
|
|
31
|
+
global: { stubs: { ImageCropper: true, EditEventsTopMenu: true, Attendance: true, MergeAccounts: true } }
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const findInput = (wrapper, label) =>
|
|
35
|
+
wrapper
|
|
36
|
+
.findAllComponents(TitledInput)
|
|
37
|
+
.find(input => input.props('title').startsWith(label))
|
|
38
|
+
.find('input');
|
|
39
|
+
|
|
40
|
+
function typeImmediately(input, value) {
|
|
41
|
+
const element = input.element;
|
|
42
|
+
element.value = value;
|
|
43
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function saveImmediately(wrapper) {
|
|
47
|
+
wrapper
|
|
48
|
+
.findAllComponents(BaseButton)
|
|
49
|
+
.find(button => button.props('label') === 'Save')
|
|
50
|
+
.element.click();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function selectTab(wrapper, tab) {
|
|
54
|
+
wrapper.findComponent(EditEventsTopMenu).vm.$emit('tabMenuClick', tab);
|
|
55
|
+
await nextTick();
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('PersonManagement draft saves', () => {
|
|
59
|
+
it('supports the first Save of a profile without names, email relations, or images', () => {
|
|
60
|
+
const wrapper = mountProfile({});
|
|
61
|
+
typeImmediately(findInput(wrapper, 'Display Name'), 'New Fencer');
|
|
62
|
+
typeImmediately(findInput(wrapper, 'Legal Name'), 'New Legal Name');
|
|
63
|
+
saveImmediately(wrapper);
|
|
64
|
+
expect(wrapper.emitted('submit:person')[0][0]).toMatchObject({
|
|
65
|
+
DisplayName: 'New Fencer',
|
|
66
|
+
LegalName: 'New Legal Name',
|
|
67
|
+
EmailAddress: '',
|
|
68
|
+
FirstName: 'New',
|
|
69
|
+
LastName: 'Legal Name'
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it.each(['mouse', 'programmatic'])('submits last-second edits for a %s Save', action => {
|
|
73
|
+
const profile = createProfile();
|
|
74
|
+
const wrapper = mountProfile(profile);
|
|
75
|
+
typeImmediately(findInput(wrapper, 'Display Name'), 'Latest Display Name');
|
|
76
|
+
typeImmediately(findInput(wrapper, 'Legal Name'), 'Latest Legal Name');
|
|
77
|
+
typeImmediately(findInput(wrapper, 'Email Address'), 'latest@example.com');
|
|
78
|
+
|
|
79
|
+
if (action === 'mouse') saveImmediately(wrapper);
|
|
80
|
+
if (action === 'programmatic') wrapper.vm.handleSubmit();
|
|
81
|
+
|
|
82
|
+
expect(wrapper.emitted('submit:person')[0][0]).toMatchObject({
|
|
83
|
+
DisplayName: 'Latest Display Name',
|
|
84
|
+
LegalName: 'Latest Legal Name',
|
|
85
|
+
FirstName: 'Latest',
|
|
86
|
+
LastName: 'Legal Name',
|
|
87
|
+
EmailAddress: 'latest@example.com'
|
|
88
|
+
});
|
|
89
|
+
expect(profile.FirstName).toBe('Original');
|
|
90
|
+
expect(profile.PersonEmails[0].EmailAddress).toBe('original@example.com');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('keeps edited legal name and email after switching tabs immediately', async () => {
|
|
94
|
+
const wrapper = mountProfile();
|
|
95
|
+
typeImmediately(findInput(wrapper, 'Legal Name'), 'Edited Legal Name');
|
|
96
|
+
typeImmediately(findInput(wrapper, 'Email Address'), 'edited@example.com');
|
|
97
|
+
await selectTab(wrapper, 'Admin');
|
|
98
|
+
typeImmediately(findInput(wrapper, 'HEMA Rating ID'), '456');
|
|
99
|
+
await selectTab(wrapper, 'Basic');
|
|
100
|
+
|
|
101
|
+
expect(findInput(wrapper, 'Legal Name').element.value).toBe('Edited Legal Name');
|
|
102
|
+
expect(findInput(wrapper, 'Email Address').element.value).toBe('edited@example.com');
|
|
103
|
+
saveImmediately(wrapper);
|
|
104
|
+
expect(wrapper.emitted('submit:person')[0][0]).toMatchObject({
|
|
105
|
+
LegalName: 'Edited Legal Name',
|
|
106
|
+
EmailAddress: 'edited@example.com',
|
|
107
|
+
HemaRatingsId: '456'
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('preserves deliberately empty draft fields instead of rereading legacy values', async () => {
|
|
112
|
+
const wrapper = mountProfile(createProfile({ LegalName: '', EmailAddress: '', HemaRatingsId: null }));
|
|
113
|
+
expect(findInput(wrapper, 'Legal Name').element.value).toBe('');
|
|
114
|
+
expect(findInput(wrapper, 'Email Address').element.value).toBe('');
|
|
115
|
+
await selectTab(wrapper, 'Admin');
|
|
116
|
+
expect(findInput(wrapper, 'HEMA Rating ID').element.value).toBe('');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('keeps a cleared HR ID blank through tab changes and submits the blank draft value', async () => {
|
|
120
|
+
const wrapper = mountProfile();
|
|
121
|
+
await selectTab(wrapper, 'Admin');
|
|
122
|
+
typeImmediately(findInput(wrapper, 'HEMA Rating ID'), '');
|
|
123
|
+
await selectTab(wrapper, 'Basic');
|
|
124
|
+
await selectTab(wrapper, 'Admin');
|
|
125
|
+
expect(findInput(wrapper, 'HEMA Rating ID').element.value).toBe('');
|
|
126
|
+
saveImmediately(wrapper);
|
|
127
|
+
expect(wrapper.emitted('submit:person')[0][0].HemaRatingsId).toBe('');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('submits an explicitly null HR ID without repopulating stale ratings', () => {
|
|
131
|
+
const wrapper = mountProfile(createProfile({ HemaRatingsId: null }));
|
|
132
|
+
saveImmediately(wrapper);
|
|
133
|
+
expect(wrapper.emitted('submit:person')[0][0].HemaRatingsId).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('submits a detached snapshot of the draft', () => {
|
|
137
|
+
const profile = createProfile();
|
|
138
|
+
const wrapper = mountProfile(profile);
|
|
139
|
+
saveImmediately(wrapper);
|
|
140
|
+
const submitted = wrapper.emitted('submit:person')[0][0];
|
|
141
|
+
submitted.Club.Name = 'Changed by parent';
|
|
142
|
+
submitted.Images.push({ URL: '/other.png' });
|
|
143
|
+
expect(profile.Club.Name).toBe('Example Club');
|
|
144
|
+
expect(wrapper.vm.person.Club.Name).toBe('Example Club');
|
|
145
|
+
expect(wrapper.vm.person.Images).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe('PersonManagement validation', () => {
|
|
150
|
+
it('does not open an unavailable Admin tab from validation feedback', async () => {
|
|
151
|
+
const wrapper = mountProfile(createProfile({ HemaRatingsId: 0 }), { userLevel: 'User' });
|
|
152
|
+
saveImmediately(wrapper);
|
|
153
|
+
await nextTick();
|
|
154
|
+
expect(wrapper.find('[data-testid="profile-validation-summary"] button').attributes('disabled')).toBeDefined();
|
|
155
|
+
wrapper.vm.handleTab('Admin');
|
|
156
|
+
expect(wrapper.findComponent(Administration).exists()).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
it('explains an invalid Basic field when Save is clicked on Admin', async () => {
|
|
159
|
+
const wrapper = mountProfile();
|
|
160
|
+
typeImmediately(findInput(wrapper, 'Email Address'), 'invalid-email');
|
|
161
|
+
await selectTab(wrapper, 'Admin');
|
|
162
|
+
typeImmediately(findInput(wrapper, 'HEMA Rating ID'), '456');
|
|
163
|
+
saveImmediately(wrapper);
|
|
164
|
+
await nextTick();
|
|
165
|
+
|
|
166
|
+
expect(wrapper.emitted('submit:person')).toBeUndefined();
|
|
167
|
+
const summary = wrapper.find('[data-testid="profile-validation-summary"]');
|
|
168
|
+
expect(summary.text()).toContain('Email address');
|
|
169
|
+
expect(summary.text()).toContain('Basic tab');
|
|
170
|
+
await summary.find('button').trigger('click');
|
|
171
|
+
expect(wrapper.findComponent(BasicEdit).exists()).toBe(true);
|
|
172
|
+
expect(findInput(wrapper, 'Email Address').element.value).toBe('invalid-email');
|
|
173
|
+
await findInput(wrapper, 'Email Address').setValue('fixed@example.com');
|
|
174
|
+
expect(wrapper.find('[data-testid="profile-validation-summary"]').exists()).toBe(false);
|
|
175
|
+
saveImmediately(wrapper);
|
|
176
|
+
expect(wrapper.emitted('submit:person')).toHaveLength(1);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it.each(['0', '-1', '1.5', '1e3', 'abc', '9007199254740992'])(
|
|
180
|
+
'rejects invalid HR ID %s without changing the typed value',
|
|
181
|
+
async value => {
|
|
182
|
+
const wrapper = mountProfile();
|
|
183
|
+
await selectTab(wrapper, 'Admin');
|
|
184
|
+
await findInput(wrapper, 'HEMA Rating ID').setValue(value);
|
|
185
|
+
expect(findInput(wrapper, 'HEMA Rating ID').element.value).toBe(value);
|
|
186
|
+
expect(findInput(wrapper, 'HEMA Rating ID').attributes('aria-invalid')).toBe('true');
|
|
187
|
+
await selectTab(wrapper, 'Basic');
|
|
188
|
+
saveImmediately(wrapper);
|
|
189
|
+
await nextTick();
|
|
190
|
+
expect(wrapper.emitted('submit:person')).toBeUndefined();
|
|
191
|
+
expect(wrapper.find('[data-testid="profile-validation-summary"]').text()).toContain('Admin tab');
|
|
192
|
+
await wrapper.find('[data-testid="profile-validation-summary"] button').trigger('click');
|
|
193
|
+
expect(wrapper.findComponent(Administration).exists()).toBe(true);
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
describe('PersonManagement draft replacement', () => {
|
|
199
|
+
it('preserves unsaved edits on incidental updates for the same person', async () => {
|
|
200
|
+
const profile = createProfile();
|
|
201
|
+
const wrapper = mountProfile(profile);
|
|
202
|
+
typeImmediately(findInput(wrapper, 'Legal Name'), 'Unsaved Legal Name');
|
|
203
|
+
await wrapper.setProps({ personProfile: { ...profile, PersonId: '1', DisplayName: 'Server Name' } });
|
|
204
|
+
expect(findInput(wrapper, 'Legal Name').element.value).toBe('Unsaved Legal Name');
|
|
205
|
+
expect(wrapper.vm.person.DisplayName).toBe('Original Fencer');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('replaces the draft and resets controls and errors when draftResetKey changes', async () => {
|
|
209
|
+
const profile = createProfile();
|
|
210
|
+
const wrapper = mountProfile(profile);
|
|
211
|
+
typeImmediately(findInput(wrapper, 'Email Address'), 'invalid');
|
|
212
|
+
saveImmediately(wrapper);
|
|
213
|
+
await selectTab(wrapper, 'Admin');
|
|
214
|
+
await wrapper.setProps({
|
|
215
|
+
personProfile: createProfile({
|
|
216
|
+
EmailAddress: 'saved@example.com',
|
|
217
|
+
HemaRatingsId: null,
|
|
218
|
+
ProfileImage: '/saved.png'
|
|
219
|
+
}),
|
|
220
|
+
draftResetKey: 1
|
|
221
|
+
});
|
|
222
|
+
expect(wrapper.find('[data-testid="profile-validation-summary"]').exists()).toBe(false);
|
|
223
|
+
expect(findInput(wrapper, 'HEMA Rating ID').element.value).toBe('');
|
|
224
|
+
expect(wrapper.findComponent(ImageCropper).props('urlToImage')).toBe('/saved.png');
|
|
225
|
+
await selectTab(wrapper, 'Basic');
|
|
226
|
+
expect(findInput(wrapper, 'Email Address').element.value).toBe('saved@example.com');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('automatically resets when a different person is selected', async () => {
|
|
230
|
+
const wrapper = mountProfile();
|
|
231
|
+
typeImmediately(findInput(wrapper, 'Legal Name'), 'Unsaved Name');
|
|
232
|
+
await selectTab(wrapper, 'Admin');
|
|
233
|
+
await wrapper.setProps({ personProfile: createProfile({ PersonId: 2, LegalName: 'Second Person', Images: [] }) });
|
|
234
|
+
expect(wrapper.findComponent(BasicEdit).exists()).toBe(true);
|
|
235
|
+
expect(findInput(wrapper, 'Legal Name').element.value).toBe('Second Person');
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('PersonManagement save state and images', () => {
|
|
240
|
+
it.each(['saving', 'disabled'])('disables editing and actions while %s', async prop => {
|
|
241
|
+
const wrapper = mountProfile(createProfile(), { [prop]: true });
|
|
242
|
+
expect(wrapper.find('fieldset').attributes('disabled')).toBeDefined();
|
|
243
|
+
expect(wrapper.find('fieldset').attributes('inert')).toBeDefined();
|
|
244
|
+
expect(wrapper.findComponent(ImageCropper).props('disabled')).toBe(true);
|
|
245
|
+
expect(wrapper.findAllComponents(BaseButton).every(button => button.attributes('disabled') !== undefined)).toBe(
|
|
246
|
+
true
|
|
247
|
+
);
|
|
248
|
+
wrapper.findComponent(BasicEdit).vm.$emit('update:person', { field: 'DisplayName', value: 'Blocked edit' });
|
|
249
|
+
wrapper.vm.handleSubmit();
|
|
250
|
+
wrapper.vm.handleDelete();
|
|
251
|
+
wrapper.vm.doMerge({ PersonId: 2 });
|
|
252
|
+
await selectTab(wrapper, 'Admin');
|
|
253
|
+
expect(wrapper.findComponent(BasicEdit).exists()).toBe(true);
|
|
254
|
+
expect(wrapper.vm.person.DisplayName).toBe('Original Fencer');
|
|
255
|
+
expect(wrapper.emitted('submit:person')).toBeUndefined();
|
|
256
|
+
expect(wrapper.emitted('delete:person')).toBeUndefined();
|
|
257
|
+
expect(wrapper.emitted('merge:merge')).toBeUndefined();
|
|
258
|
+
await wrapper.setProps({ [prop]: false });
|
|
259
|
+
saveImmediately(wrapper);
|
|
260
|
+
expect(wrapper.emitted('submit:person')).toHaveLength(1);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('announces the saving state accessibly', () => {
|
|
264
|
+
const wrapper = mountProfile(createProfile(), { saving: true });
|
|
265
|
+
expect(wrapper.find('#person-management').attributes('aria-busy')).toBe('true');
|
|
266
|
+
expect(wrapper.find('[role="status"]').text()).toContain('Saving profile');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it.each([undefined, []])('handles missing or empty persisted images (%j)', images => {
|
|
270
|
+
const wrapper = mountProfile(createProfile({ Images: images }));
|
|
271
|
+
expect(wrapper.findComponent(ImageCropper).props('urlToImage')).toBe('');
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('previews an uploaded image and includes it only in the next explicit Save', async () => {
|
|
275
|
+
const wrapper = mountProfile(createProfile({ Images: [{ Type: 'Portrait', URL: '/old.png' }] }));
|
|
276
|
+
const cropper = wrapper.findComponent(ImageCropper);
|
|
277
|
+
expect(cropper.props('urlToImage')).toBe('/old.png');
|
|
278
|
+
cropper.vm.$emit('update:uploading', true);
|
|
279
|
+
await nextTick();
|
|
280
|
+
saveImmediately(wrapper);
|
|
281
|
+
expect(wrapper.emitted('submit:person')).toBeUndefined();
|
|
282
|
+
cropper.vm.$emit('update:url', 'https://images.example/new.png');
|
|
283
|
+
cropper.vm.$emit('update:uploading', false);
|
|
284
|
+
await nextTick();
|
|
285
|
+
expect(cropper.props('urlToImage')).toBe('https://images.example/new.png');
|
|
286
|
+
expect(wrapper.emitted('update:image')).toEqual([['https://images.example/new.png']]);
|
|
287
|
+
expect(wrapper.emitted('submit:person')).toBeUndefined();
|
|
288
|
+
saveImmediately(wrapper);
|
|
289
|
+
expect(wrapper.emitted('submit:person')[0][0].ProfileImage).toBe('https://images.example/new.png');
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('retains an upload started before the parent disables editing', async () => {
|
|
293
|
+
const wrapper = mountProfile();
|
|
294
|
+
const cropper = wrapper.findComponent(ImageCropper);
|
|
295
|
+
cropper.vm.$emit('update:uploading', true);
|
|
296
|
+
await wrapper.setProps({ disabled: true });
|
|
297
|
+
cropper.vm.$emit('update:url', 'https://images.example/finished.png');
|
|
298
|
+
cropper.vm.$emit('update:uploading', false);
|
|
299
|
+
await wrapper.setProps({ disabled: false });
|
|
300
|
+
saveImmediately(wrapper);
|
|
301
|
+
expect(wrapper.emitted('submit:person')[0][0].ProfileImage).toBe('https://images.example/finished.png');
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
it('ignores late image events after a draft reset', async () => {
|
|
305
|
+
const wrapper = mountProfile();
|
|
306
|
+
const cropper = wrapper.findComponent(ImageCropper);
|
|
307
|
+
await wrapper.setProps({ draftResetKey: 1 });
|
|
308
|
+
cropper.vm.$emit('update:url', 'https://images.example/stale.png');
|
|
309
|
+
expect(wrapper.vm.person.ProfileImage).toBeUndefined();
|
|
310
|
+
expect(wrapper.emitted('update:image')).toBeUndefined();
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it.each(['load', 'upload'])('reports an image %s failure without changing the draft', async type => {
|
|
314
|
+
const wrapper = mountProfile(createProfile({ ProfileImage: '/draft.png' }));
|
|
315
|
+
const cropper = wrapper.findComponent(ImageCropper);
|
|
316
|
+
cropper.vm.$emit('update:uploading', true);
|
|
317
|
+
cropper.vm.$emit(`error:${type}`, { message: 'Image failure' });
|
|
318
|
+
cropper.vm.$emit('update:url', null);
|
|
319
|
+
await nextTick();
|
|
320
|
+
expect(wrapper.vm.person.ProfileImage).toBe('/draft.png');
|
|
321
|
+
expect(wrapper.emitted('update:image')).toBeUndefined();
|
|
322
|
+
expect(wrapper.emitted('error:image')).toEqual([[{ type, message: 'Image failure' }]]);
|
|
323
|
+
expect(wrapper.find('[role="alert"]').text()).toBe('Image failure');
|
|
324
|
+
if (type === 'upload') {
|
|
325
|
+
saveImmediately(wrapper);
|
|
326
|
+
expect(wrapper.emitted('submit:person')[0][0].ProfileImage).toBe('/draft.png');
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
});
|