fcad-core-dragon 2.2.0-beta.2 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG CHANGED
@@ -1,3 +1,6 @@
1
+ 2.2.0(21 janvier 2026)
2
+ Voir corrections des versions 2.2.0-beta
3
+
1
4
  2.2.0-beta.2(8 janvier 2026)
2
5
  Correction du comportement de la playbar du lecteur vidéo (elle reste toujours visible, sauf en plein écran)
3
6
 
@@ -82,4 +82,4 @@ Error generating stack: `+n.message+`
82
82
  <div id='root'></div>
83
83
  </body>
84
84
  </html>
85
- <script id="playwrightReportBase64" type="application/zip">data:application/zip;base64,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</script>
85
+ <script id="playwrightReportBase64" type="application/zip">data:application/zip;base64,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</script>
package/package.json CHANGED
@@ -21,6 +21,7 @@
21
21
  "@pinia/testing": "^1.0.3",
22
22
  "@playwright/experimental-ct-vue": "^1.56.1",
23
23
  "@playwright/test": "^1.56.1",
24
+ "@testing-library/jest-dom": "^6.9.1",
24
25
  "@types/node": "^24.10.0",
25
26
  "@vitejs/plugin-vue": "^6.0.1",
26
27
  "@vitest/coverage-v8": "^4.0.15",
@@ -65,5 +66,5 @@
65
66
  "watch": "nodemon -e js,vue,html,json -x yalc publish --push"
66
67
  },
67
68
  "type": "module",
68
- "version": "2.2.0-beta.2"
69
+ "version": "2.2.0"
69
70
  }
@@ -1,6 +1,6 @@
1
1
  <!--
2
- @ Description: This component is used to display and create the link's to all the activity creation in module.
3
- @ What it does: Goes trougth all the activity in the router and create a card that open the Table of content (appCompTableOfContent) that display the anchor.Display the title and subtitle enter in menu.json. Must be used with AppCompTableOfContent and AppCompMenu.
2
+ @ Description: This component is used to display and create the links for all the activities in module.
3
+ @ What it does: Goes through all the activities in the router and create a card that navigate to the corresponding activity. Display the title and subtitle enter in menu.json. Must be used with AppCompTableOfContent and AppCompMenu.
4
4
  -->
5
5
  <template>
6
6
  <v-row v-if="activities.length" class="box-msa">
@@ -70,6 +70,7 @@
70
70
  v-for="(credit, index) of credits"
71
71
  :key="`credit_${index + 1}`"
72
72
  :ref="`#nt_${index + 1}`"
73
+ class="item-credits"
73
74
  v-html="credit"
74
75
  ></li>
75
76
  </ul>
@@ -79,7 +80,7 @@
79
80
  </div>
80
81
  </template>
81
82
  <script>
82
- import { mapState, mapActions } from 'pinia'
83
+ import { mapActions } from 'pinia'
83
84
  import { useAppStore } from '../module/stores/appStore'
84
85
  import { validateObjType } from '../shared/validators'
85
86
  import { useI18n } from 'vue-i18n'
@@ -99,17 +100,11 @@ export default {
99
100
  shouldDeactivate: false,
100
101
  errorData: [],
101
102
  notes: null,
102
- credits: null
103
+ credits: null,
104
+ test: null
103
105
  }
104
106
  },
105
107
  computed: {
106
- ...mapState(useAppStore, [
107
- 'getDataNoteCredit',
108
- 'getAllActivities',
109
- 'getCurrentPage',
110
- 'getNotes',
111
- 'getCredits'
112
- ]),
113
108
  hasNotes() {
114
109
  return this.notes && this.notes.length
115
110
  },
@@ -156,7 +151,6 @@ export default {
156
151
  //Add all the note to the temp array
157
152
  formatedNotes.push(...Object.values(g)[0])
158
153
  })
159
-
160
154
  return formatedNotes
161
155
  },
162
156
 
@@ -174,6 +168,7 @@ export default {
174
168
  },
175
169
  setPageNotesAndCredits(data) {
176
170
  const { notes, credits } = data
171
+
177
172
  this.notes = this.getPageNotes(notes)
178
173
  this.credits = this.getPageCredits(credits)
179
174
  },
@@ -186,7 +181,6 @@ export default {
186
181
  validateData(ctx, data) {
187
182
  let errConsole = []
188
183
  if (this.errorData.length) this.errorData = [] //reset the error tracker
189
-
190
184
  switch (ctx) {
191
185
  case 'note': {
192
186
  if (data.constructor !== Object) {
@@ -197,7 +191,6 @@ export default {
197
191
  `Unexpected definition the note. Expecting Object but received ${typeof data}`
198
192
  )
199
193
  }
200
-
201
194
  let stringType = ['id', 'text'] //expected attribute in the note declaration
202
195
 
203
196
  let { errorInConsole, errorList } = validateObjType(
@@ -208,7 +201,6 @@ export default {
208
201
  null,
209
202
  `Note ${data.details}`
210
203
  )
211
-
212
204
  if (errorInConsole.length) errConsole.push(...errorInConsole)
213
205
  if (errorList.length) this.errorData.push(...errorList)
214
206
  break
@@ -359,6 +351,10 @@ export default {
359
351
  const elTarget = widgetContent.namedItem(ref) // Target current button
360
352
  const pageRef = elTarget.getAttribute('pageref')
361
353
 
354
+ console.log('widgetContent :', widgetContent)
355
+ console.log('elTarget :', elTarget)
356
+ console.log('pageRef :', pageRef)
357
+
362
358
  if (!this.sideBarIsOpen) this.prevNote = null //reset previous when sidebar is opened
363
359
 
364
360
  let prevPageref = this.prevNote
@@ -27,7 +27,6 @@ export const useAppStore = defineStore('$appStore', {
27
27
  userMetaData: {},
28
28
  appConfigs: null,
29
29
  menuSetting: null,
30
- dataNc: {},
31
30
  wigIsOpen: false,
32
31
  popIsOpen: false,
33
32
  sideBIsOpen: false,
@@ -596,9 +595,6 @@ export const useAppStore = defineStore('$appStore', {
596
595
  getDataFromServer: (state) => {
597
596
  return state.dataFromServer
598
597
  },
599
- getDataNoteCredit: (state) => {
600
- return state.dataNc
601
- },
602
598
  getWidgetOpen(state) {
603
599
  return state.wigIsOpen
604
600
  },
@@ -801,9 +797,6 @@ export const useAppStore = defineStore('$appStore', {
801
797
  )
802
798
  this.currentSection = data
803
799
  },
804
- updateCurrentnoteCredit(data) {
805
- this.dataNc = data
806
- },
807
800
  /**
808
801
  * @description: Save user interaction on a page to store
809
802
  * USe to Update existing userMetadata
@@ -955,18 +955,9 @@ const validateQuizData = (data) => {
955
955
  if (hasError) return errors
956
956
 
957
957
  //validate Containt for retroaction
958
-
959
-
960
- // if(data.solution !=null){
961
- // console.log('ici')
962
- // }else{
963
- // console.log('la')
964
- // }
965
-
966
958
  switch (true) {
967
959
  case data.retroaction != null:
968
960
  objectType = ['retro_positive', 'retro_negative', 'retro_neutre']
969
-
970
961
 
971
962
  errors = validateObjType(
972
963
  data.retroaction,
@@ -983,19 +974,15 @@ const validateQuizData = (data) => {
983
974
  // Validate containt of retroaction attributes
984
975
  stringType = ['title', 'hypertext']
985
976
 
986
-
987
977
  for (const key of Object.keys(data.retroaction)) {
988
-
989
- if(key == "retro_neutre") continue //skip
990
-
991
-
992
-
993
- errors = validateObjType(
994
- data.retroaction[key],
995
- { stringType },
996
- null,
997
- key
998
- )
978
+ if (key == 'retro_neutre') continue //skip
979
+
980
+ errors = validateObjType(
981
+ data.retroaction[key],
982
+ { stringType },
983
+ null,
984
+ key
985
+ )
999
986
 
1000
987
  hasError = errors.errorInConsole.length || errors.errorList.length
1001
988
 
@@ -1,32 +1,45 @@
1
1
  import { mount } from '@vue/test-utils'
2
- import { beforeEach, describe, expect, vi, test } from 'vitest'
2
+ import { beforeEach, describe, expect, vi, test, it } from 'vitest'
3
3
  import { createTestingPinia } from '@pinia/testing'
4
4
 
5
- const dummyProps = [
5
+ const dummyPropsNotes = [
6
6
  {
7
- id: 'nt_1',
8
- text: `note 1`
9
- },
10
- {
11
- id: 'nt_2',
12
- text: `note 2`
7
+ P01: [
8
+ {
9
+ id: 'nt_1',
10
+ text: 'note 1',
11
+ page_ref: 'P01'
12
+ },
13
+ {
14
+ id: 'nt_2',
15
+ text: 'note 2',
16
+ page_ref: 'P01'
17
+ }
18
+ ]
13
19
  }
14
20
  ]
15
21
 
22
+ const dummyPropsCredits = ['credits 1', 'credits 2', 'credits 3']
23
+
16
24
  //Mock store appStore
17
25
  vi.mock('@/module/stores/appStore', () => ({
18
- useAppStore: () => ({
19
- getDataNoteCredit: dummyProps,
20
- getAllActivities: 'A01',
21
- getCurrentPage: 'P01'
26
+ useAppStore: () => ({ updateWidgetOpen: vi.fn() })
27
+ }))
28
+
29
+ //Mock Error handeling
30
+ vi.mock('@/shared/validators.js', () => ({
31
+ validateObjType: vi.fn(() => {
32
+ return { errorInConsole: [], errorList: [] }
22
33
  })
23
34
  }))
35
+
24
36
  // Import of components must happen after mocks to prevent side effects
25
37
  import AppCompNoteCredit from '../../src/components/AppCompNoteCredit.vue'
26
38
 
27
39
  describe('AppCompNoteCredit', () => {
28
40
  let wrapper = null
29
41
 
42
+ //Mounting componant
30
43
  beforeEach(() => {
31
44
  wrapper = mount(AppCompNoteCredit, {
32
45
  plugins: [createTestingPinia({ createSpy: vi.fn })],
@@ -34,16 +47,118 @@ describe('AppCompNoteCredit', () => {
34
47
  })
35
48
  })
36
49
 
37
- test('It renders a the pop-up', () => {
50
+ test('It renders componant with notes and credits', async () => {
51
+ const wrapper = mount(AppCompNoteCredit)
52
+
53
+ //Give information
54
+ const d = {
55
+ type: 'noteCredit',
56
+ content: { notes: dummyPropsNotes, credits: dummyPropsCredits }
57
+ }
58
+
59
+ //Filling the componant
60
+ await wrapper.vm.onToggleWidget(d)
61
+ expect(wrapper.exists()).toBe(true)
62
+
63
+ //Find all the information that was renders
64
+ const ctnNotes = wrapper.find('#notes-list')
65
+ const ctnCredits = wrapper.find('#credits-list')
66
+
67
+ //Expect test result
68
+ expect(ctnNotes.exists()).toBe(true)
69
+ expect(ctnCredits.exists()).toBe(true)
70
+ })
71
+
72
+ test('It renders componant with only notes', async () => {
38
73
  const wrapper = mount(AppCompNoteCredit)
39
74
 
75
+ //Give information
76
+ const d = {
77
+ type: 'noteCredit',
78
+ content: { notes: dummyPropsNotes, credits: null }
79
+ }
80
+
81
+ //Filling the componant
82
+ await wrapper.vm.onToggleWidget(d)
40
83
  expect(wrapper.exists()).toBe(true)
41
84
 
42
- const div = wrapper.find('#noteCredit')
43
- expect(div.exists()).toBe(true)
85
+ //Find all the information that was renders
86
+ const divNotes = wrapper.find('#notes-list')
87
+ const divCredits = wrapper.find('#credits-list')
88
+
89
+ //Expect test result
90
+ expect(divNotes.exists()).toBe(true)
91
+ expect(divCredits.exists()).toBe(false)
44
92
  })
45
93
 
46
- test('It renders a list of ${dummyProps.length}', () => {
47
- expect(wrapper.findAll('div').length).toBe(dummyProps.length)
94
+ test('It renders componant with only credits', async () => {
95
+ const wrapper = mount(AppCompNoteCredit)
96
+
97
+ //Give information
98
+ const d = {
99
+ type: 'noteCredit',
100
+ content: { notes: null, credits: dummyPropsCredits }
101
+ }
102
+
103
+ //Filling the componant
104
+ await wrapper.vm.onToggleWidget(d)
105
+ expect(wrapper.exists()).toBe(true)
106
+
107
+ //Find all the information that was renders
108
+ const divNotes = wrapper.find('#notes-list')
109
+ const divCredits = wrapper.find('#credits-list')
110
+
111
+ //Expect test result
112
+ expect(divNotes.exists()).toBe(false)
113
+ expect(divCredits.exists()).toBe(true)
114
+ })
115
+
116
+ test('It renders a list of present notes and credits', async () => {
117
+ const wrapper = mount(AppCompNoteCredit)
118
+
119
+ //Give information
120
+ const d = {
121
+ type: 'noteCredit',
122
+ content: { notes: dummyPropsNotes, credits: dummyPropsCredits }
123
+ }
124
+
125
+ //Filling the componant
126
+ await wrapper.vm.onToggleWidget(d)
127
+
128
+ //Find all the information that was renders
129
+ const notes = Object.values(dummyPropsNotes[0])
130
+
131
+ //Expect test result
132
+ expect(wrapper.findAll('.note-item').length).toBe(notes[0].length)
133
+ expect(wrapper.findAll('.item-credits').length).toBe(
134
+ dummyPropsCredits.length
135
+ )
136
+ })
137
+
138
+ test('It render all the rigth information in button data', async () => {
139
+ //Give information
140
+ const d = {
141
+ type: 'noteCredit',
142
+ content: { notes: dummyPropsNotes }
143
+ }
144
+
145
+ //Filling the componant
146
+ await wrapper.vm.onToggleWidget(d)
147
+
148
+ //Find all the information that was renders
149
+ const notes = Object.values(dummyPropsNotes[0])
150
+ const note = notes[0][0]
151
+ const div = wrapper.find(`#nt_${note.page_ref}__${note.id.substring(3)}`)
152
+
153
+ //Expect test result
154
+ expect(div.attributes('note-ref')).toBe(
155
+ `rnt_${note.page_ref}__${note.id.substring(3)}`
156
+ )
157
+
158
+ expect(div.attributes('data-ref')).toBe(
159
+ `rnt_${note.page_ref}__${note.id.substring(3)}`
160
+ )
161
+
162
+ expect(div.attributes('pageref')).toBe(note.page_ref)
48
163
  })
49
164
  })
@@ -0,0 +1,428 @@
1
+ import { mount } from '@vue/test-utils'
2
+ import { beforeEach, afterAll, describe, expect, it, vi } from 'vitest'
3
+ import { createTestingPinia } from '@pinia/testing'
4
+
5
+ //TODO:
6
+ // 1- test pour valider que le composant rend le bon player selon le type de media (audio/video)
7
+ // 1b- verfifier recuperation du bon media media data(getCUrrentPage.mElements[i])
8
+ // 2- test pour valider le trigger des evenemets:
9
+ // - play & pause (toggglePlay): OK
10
+ // - ended (mediaEnded): OK
11
+ // - volume change (updateVolumeLevel): OK
12
+ // - mute toggle (toggleMute): OK
13
+ // - fullscreen toggle (toggleFullscreen): A FAIRE
14
+ // 3- test pour valider l'affichage du transcript :OK
15
+
16
+ //Helper to create a mock of media container with event listeners
17
+ const makeMediaContainer = () => {
18
+ const listeners = new Map()
19
+ return {
20
+ addEventListener: vi.fn((name, fn) => listeners.set(name, fn)),
21
+ removeEventListener: vi.fn((name) => listeners.delete(name)),
22
+ requestFullscreen: vi.fn(async () => {}),
23
+ dispatchEvent: (name, payload) => listeners.get(name)?.(payload)
24
+ }
25
+ }
26
+
27
+ //Helper to create a mock of media element with event listeners and methods
28
+ const makeMediaElement = (id, type, duration) => {
29
+ const listeners = new Map()
30
+ const el = {
31
+ id,
32
+ tagName: type.toUpperCase(),
33
+ duration,
34
+ currentTime: 0,
35
+ volume: 0.5,
36
+ muted: false,
37
+ paused: true,
38
+ play: vi.fn(async () => {
39
+ el.paused = false
40
+ }),
41
+ pause: vi.fn(() => {
42
+ el.paused = true
43
+ }),
44
+ addEventListener: vi.fn((name, fn) => listeners.set(name, fn)),
45
+ removeEventListener: vi.fn((name) => listeners.delete(name)),
46
+ dispatchEvent: (name, payload) => listeners.get(name)?.(payload)
47
+ }
48
+ return el
49
+ }
50
+
51
+ let defineMediaToPlay = (type, duration) => {
52
+ if (type === 'video') {
53
+ const id = 'vid1'
54
+ return {
55
+ id,
56
+ mElement: makeMediaElement(id, type, duration || 20),
57
+ mMediaContainer: makeMediaContainer(),
58
+ mSubtitles: {},
59
+ mTranscript: 'exemple_transcript.html',
60
+ mType: 'video'
61
+ }
62
+ }
63
+
64
+ if (type === 'audio') {
65
+ const id = 'aud1'
66
+ return {
67
+ id,
68
+ mElement: makeMediaElement(id, type, duration || 30),
69
+ mTranscript:
70
+ '<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>',
71
+ mType: 'audio'
72
+ }
73
+ }
74
+ }
75
+
76
+ //Helper to return the sctructure of an HTMLElement
77
+ const getStructure = (el) => ({
78
+ tag: el && el.tagName ? el.tagName.toLowerCase() : null,
79
+ class: el.className || null,
80
+ children:
81
+ el.children.length > 0 ? [...el.children].map((c) => getStructure(c)) : []
82
+ })
83
+
84
+ const mockedStore = {
85
+ getCurrentBrowser: () => 'chrome',
86
+ getPageInteraction: vi.fn(() => ({
87
+ userActivation: {
88
+ mediasViewed: []
89
+ }
90
+ })),
91
+ getUserInteraction: () => ({}),
92
+ getModuleInfo: { id: 'MOD-1' },
93
+ getCurrentPage: {
94
+ id: 'P01',
95
+ activityRef: 'A03',
96
+ title: 'Lecteurs médias',
97
+ type: 'pg_normal',
98
+ audiosData: [
99
+ {
100
+ id: 'aud1',
101
+ aSources: [{ type: 'mp3', src: '/audios/aud1.mp3' }],
102
+ aTranscript: 'exemple_transcript.html',
103
+ mSources: [{ src: '/audios/aud1.mp3' }],
104
+ mTitle: 'Audio 1'
105
+ }
106
+ ],
107
+ videosData: [
108
+ {
109
+ id: 'vid1',
110
+ mSources: [{ src: '/videos/vid1.mp4' }],
111
+ mTitle: 'Video 1'
112
+ }
113
+ ]
114
+ },
115
+ getMedidaMuted: () => false,
116
+ getMediaSubTitles: () => false,
117
+ getCurrentMediaDuration: () => 20,
118
+ getMediaPlaybarValues: (key) => {
119
+ if (key === 'volume') return 0.5
120
+ return null
121
+ },
122
+ setMediaPlaybarValues: vi.fn(),
123
+ updateCurrentMediaElements: vi.fn(),
124
+ setMediaMuted: vi.fn(),
125
+ setMediaSubTitles: vi.fn()
126
+ }
127
+
128
+ /*
129
+ * Helper to flush all pending promises
130
+ * Useful when mocking axios calls or other async operations
131
+ * This ensures that all microtasks are completed before proceeding with assertions
132
+ */
133
+ const flush = async () => {
134
+ await Promise.resolve()
135
+ await Promise.resolve()
136
+ }
137
+
138
+ //Mock store appStore
139
+ vi.mock('@/module/stores/appStore', () => ({
140
+ useAppStore: () => mockedStore
141
+ }))
142
+
143
+ //Mock axios
144
+ vi.mock('axios', () => ({
145
+ default: {
146
+ get: vi.fn(async () => ({
147
+ data: {
148
+ // component calls: res.data.text()
149
+ text: async () => '<div id="mock-transcript">Transcript OK</div>'
150
+ }
151
+ }))
152
+ }
153
+ }))
154
+
155
+ import AppCompPlayBarNext from '@/components/AppCompPlayBarNext.vue'
156
+ import axios from 'axios'
157
+
158
+ describe('AppCompPlayBarNext', () => {
159
+ let globalConfig
160
+ let playBarContainer
161
+ let wrapper
162
+ const mockContainerRef = { className: 'pb-container' }
163
+ let $bus
164
+ let $analytics
165
+
166
+ beforeEach(() => {
167
+ vi.clearAllMocks()
168
+ $bus = {
169
+ $emit: vi.fn(),
170
+ $on: vi.fn(),
171
+ $off: vi.fn()
172
+ }
173
+ $analytics = {
174
+ sendEvent: vi.fn()
175
+ }
176
+ globalConfig = {
177
+ plugins: [createTestingPinia({ createSpy: vi.fn })],
178
+ provide: {
179
+ userInteraction: {}
180
+ },
181
+ stubs: {
182
+ // Replace custom button component with a real <button>
183
+ 'app-base-button': {
184
+ template: '<button><slot /></button>'
185
+ }
186
+ },
187
+ mocks: {
188
+ $router: {
189
+ currentRoute: {
190
+ value: {
191
+ meta: {
192
+ activity_ref: 'A03',
193
+ id: 'P01'
194
+ }
195
+ }
196
+ }
197
+ },
198
+ $bus,
199
+ $analytics,
200
+ computed: {
201
+ mediaDuration: vi.fn(() => 20)
202
+ }
203
+ }
204
+ }
205
+ })
206
+
207
+ afterAll(() => {
208
+ wrapper.unmount()
209
+ })
210
+
211
+ describe('It renders the correct HtmlElement', () => {
212
+ it('Renders the correct player instance (id = plyr_<mediaId>) for video', async () => {
213
+ const mediaType = 'video'
214
+ wrapper = mount(AppCompPlayBarNext, {
215
+ props: {
216
+ mediaToPlay: defineMediaToPlay(mediaType)
217
+ }
218
+ })
219
+
220
+ await wrapper.vm.$nextTick()
221
+ //Should render a div with class pb-container, div with class video and id plyr_vid1
222
+ playBarContainer = wrapper.find(`.pb-container`)
223
+ const playBarElement = playBarContainer.find(`.${mediaType}`)
224
+ expect(playBarElement.exists()).toBe(true)
225
+ expect(wrapper.vm.id).toBe('plyr_vid1')
226
+ })
227
+
228
+ it('Renders a the correct player instance (id = plyr_<mediaId>) for audio', async () => {
229
+ const mediaType = 'audio'
230
+ wrapper = mount(AppCompPlayBarNext, {
231
+ props: {
232
+ mediaToPlay: defineMediaToPlay(mediaType)
233
+ }
234
+ })
235
+
236
+ await wrapper.vm.$nextTick()
237
+ //Should render a div with class pb-container div with class video and id if plyr_aud1
238
+ playBarContainer = wrapper.find(`.pb-container`)
239
+ const playBarElement = playBarContainer.find(`.${mediaType}`)
240
+ expect(playBarElement.exists()).toBe(true)
241
+ expect(wrapper.vm.id).toBe('plyr_aud1')
242
+ })
243
+ })
244
+
245
+ describe('It initializes correctly', async () => {
246
+ it('Calls updateCurrentMediaElements() on mount', () => {
247
+ wrapper = mount(AppCompPlayBarNext, {
248
+ props: {
249
+ mediaToPlay: defineMediaToPlay('audio')
250
+ }
251
+ })
252
+
253
+ expect(mockedStore.updateCurrentMediaElements).toHaveBeenCalledTimes(1)
254
+ expect(mockedStore.updateCurrentMediaElements).toHaveBeenCalledWith(
255
+ expect.objectContaining({ id: wrapper.vm.mediaToPlay.id })
256
+ )
257
+ })
258
+
259
+ it('Video loads transcript on mount (setTranscript -> axios.get) when mTranscript exists', async () => {
260
+ wrapper = mount(AppCompPlayBarNext, {
261
+ props: {
262
+ mediaToPlay: defineMediaToPlay('video')
263
+ }
264
+ })
265
+ await flush() // wait for all pending promises to resolve before making assertions
266
+ expect(axios.get).toHaveBeenCalledTimes(1)
267
+ // when mTranscript has no '/', component builds "./<file>"
268
+ // expect component to normalize the path
269
+ expect(axios.get).toHaveBeenCalledWith(
270
+ `./${wrapper.vm.mediaToPlay.mTranscript}`,
271
+ expect.any(Object)
272
+ )
273
+ await wrapper.vm.$nextTick()
274
+ expect(wrapper.vm.transcriptToShow).toContain('mock-transcript')
275
+ })
276
+ })
277
+
278
+ describe('Controls media events with user interaction on:', () => {
279
+ it('PLAY: (togglePlay), emits xAPI + GA play tracking ', async () => {
280
+ await flush()
281
+ const mediaType = 'video'
282
+ wrapper = await mount(AppCompPlayBarNext, {
283
+ props: {
284
+ mediaToPlay: defineMediaToPlay(mediaType)
285
+ },
286
+ global: { ...globalConfig }
287
+ })
288
+ await flush()
289
+ const mediaID = wrapper.vm.mediaToPlay.id
290
+ expect(wrapper.vm.mediaRawData.id).toBe(mediaID)
291
+ expect(wrapper.vm.mediaToPlay.mElement.paused).toBe(true)
292
+
293
+ //Trigger play event call togglePlay()
294
+ await wrapper.vm.togglePlay()
295
+ expect(wrapper.vm.mediaToPlay.mElement.play).toHaveBeenCalledTimes(1)
296
+ expect(wrapper.vm.mediaToPlay.mElement.paused).toBe(false)
297
+ await flush()
298
+
299
+ //Check that xAPI and GA events were sent
300
+ expect($bus.$emit).toHaveBeenCalledWith(
301
+ 'send-xapi-statement',
302
+ expect.objectContaining({ verb: 'played' })
303
+ )
304
+
305
+ const url = mediaType == 'audio' ? '/audios/aud1.mp3' : '/videos/vid1.mp4'
306
+
307
+ expect($analytics.sendEvent).toHaveBeenCalledWith(
308
+ `fcad_${mediaType}_play`,
309
+ expect.objectContaining({ url })
310
+ )
311
+ })
312
+
313
+ it('STOP/END : mediaEnded(), set canReplay(), media-viewed event and emits xAPI + GA event ', async () => {
314
+ const mediaType = 'audio'
315
+ wrapper = await mount(AppCompPlayBarNext, {
316
+ props: {
317
+ mediaToPlay: defineMediaToPlay(mediaType)
318
+ },
319
+ global: { ...globalConfig }
320
+ })
321
+ await flush()
322
+ const mediaID = wrapper.vm.mediaToPlay.id
323
+ expect(wrapper.vm.mediaRawData.id).toBe(mediaID)
324
+ expect(wrapper.vm.mediaToPlay.mElement.paused).toBe(true)
325
+
326
+ //Trigger play event and ended event
327
+ await wrapper.vm.togglePlay()
328
+ expect(wrapper.vm.isPlaying).toBe(true)
329
+ wrapper.vm.mediaEnded()
330
+ await flush()
331
+ //Assert media ended events & states
332
+ expect(wrapper.vm.mediaToPlay.mElement.pause).toHaveBeenCalledTimes(1)
333
+ expect(wrapper.vm.isPlaying).toBe(false)
334
+ expect(wrapper.vm.canReplay).toBe(true)
335
+ //Assert media viewed recorded in store
336
+ expect($bus.$emit).toHaveBeenCalledWith('media-viewed', mediaID)
337
+ //Check that xAPI and GA events were sent
338
+ expect($bus.$emit).toHaveBeenCalledWith(
339
+ 'send-xapi-statement',
340
+ expect.objectContaining({ verb: 'completed' })
341
+ )
342
+ const url = mediaType == 'audio' ? '/audios/aud1.mp3' : '/videos/vid1.mp4'
343
+ expect($analytics.sendEvent).toHaveBeenCalledWith(
344
+ `fcad_${mediaType}_viewed`,
345
+ expect.objectContaining({ url })
346
+ )
347
+ })
348
+
349
+ it('VOLUME/MUTE: updateVolumeLevel() persists to store, toggleMute updates store muted state', async () => {
350
+ const mediaType = 'audio'
351
+ wrapper = await mount(AppCompPlayBarNext, {
352
+ props: {
353
+ mediaToPlay: defineMediaToPlay(mediaType)
354
+ },
355
+ global: { ...globalConfig }
356
+ })
357
+ await flush()
358
+
359
+ // updateVolumeLevel is called on mount; ensure store called at least once
360
+ expect(mockedStore.setMediaPlaybarValues).toHaveBeenCalled()
361
+ //Trigger mute action by calling toggleMute()
362
+ wrapper.vm.toggleMute()
363
+ await flush()
364
+
365
+ expect(mockedStore.setMediaMuted).toHaveBeenCalled()
366
+ expect(typeof wrapper.vm.muted).toBe('boolean')
367
+ })
368
+
369
+ it('SHOW TRANSCRIPT- toggleViewTranscript(e) emits resize-video(sm) and opens sidebar', async () => {
370
+ const mediaType = 'video'
371
+ wrapper = await mount(AppCompPlayBarNext, {
372
+ props: {
373
+ mediaToPlay: defineMediaToPlay(mediaType)
374
+ },
375
+ global: { ...globalConfig }
376
+ })
377
+ await flush()
378
+ //Assert trancript initial state
379
+ expect(wrapper.vm.transcriptEnabled).toBe(false) //disabled
380
+ expect(wrapper.vm.transcriptToShow).toBeTruthy()
381
+ //Trigger view transcript
382
+ wrapper.vm.toggleViewTranscript(true) //open transcript
383
+ expect(wrapper.vm.transcriptEnabled).toBe(true) //transcript enabled
384
+ await flush()
385
+ //Assert emits event video-transcript-toggle with correct value
386
+ expect($bus.$emit).toHaveBeenCalledWith(
387
+ 'video-transcript-toggle',
388
+ expect.objectContaining({ id: 'vid1' }),
389
+ true
390
+ )
391
+ //Assert emits resize-video(sm) and open-sidebar
392
+ const emits = wrapper.emitted('resize-video') || []
393
+ if (emits[0]) {
394
+ expect(emits[0]).toEqual(['sm'])
395
+ expect($bus.$emit).toHaveBeenCalledWith(
396
+ 'open-sidebar',
397
+ expect.objectContaining({ ctx: 'ctxTranscript' })
398
+ )
399
+ }
400
+ })
401
+
402
+ it('HIDE TRANSCRIPT- toggleViewTranscript(e), closes sidebar and emits resize-video(lg)', async () => {
403
+ const mediaType = 'video'
404
+ wrapper = await mount(AppCompPlayBarNext, {
405
+ props: {
406
+ mediaToPlay: defineMediaToPlay(mediaType)
407
+ },
408
+ global: { ...globalConfig }
409
+ })
410
+ await flush()
411
+ //Assert transcript exists
412
+ expect(wrapper.vm.transcriptToShow).toBeTruthy()
413
+ wrapper.vm.toggleViewTranscript(true) // open
414
+ await flush()
415
+ wrapper.vm.toggleViewTranscript(true) // close
416
+ //Assert transcript disabled
417
+ expect(wrapper.vm.transcriptEnabled).toBe(false)
418
+
419
+ await flush()
420
+ //Assert emits resize-video(lg) and close-sidebar
421
+ const emits = wrapper.emitted('resize-video') || []
422
+ expect(emits[0]).toEqual(['sm'])
423
+ expect(emits[1]).toEqual(['lg'])
424
+
425
+ expect($bus.$emit).toHaveBeenCalledWith('close-sidebar', 'ctxTranscript')
426
+ })
427
+ })
428
+ })
package/vitest.setup.js CHANGED
@@ -7,6 +7,7 @@ import * as components from 'vuetify/components'
7
7
  import * as directives from 'vuetify/directives'
8
8
  import * as frMessages from './src/$locales/fr.json'
9
9
  import * as enMessages from './src/$locales/en.json'
10
+ import '@testing-library/jest-dom/vitest'
10
11
  import bus from './src/plugins/bus'
11
12
  import { createPinia, setActivePinia } from 'pinia'
12
13
 
@@ -86,11 +87,11 @@ config.global.mocks = {
86
87
  id: 'P01'
87
88
  }
88
89
  },
89
- $bus: {
90
- $on: vi.fn(),
91
- $emit: vi.fn(),
92
- $off: vi.fn()
93
- },
90
+ // $bus: {
91
+ // $on: vi.fn(),
92
+ // $emit: vi.fn(),
93
+ // $off: vi.fn()
94
+ // },
94
95
  $helper: {
95
96
  formatTime: vi.fn((t) => `00:${t}`)
96
97
  }
@@ -1,8 +0,0 @@
1
- {
2
- "recommendations": [
3
- "Vue.volar",
4
- "dbaeumer.vscode-eslint",
5
- "EditorConfig.EditorConfig",
6
- "esbenp.prettier-vscode"
7
- ]
8
- }
@@ -1,16 +0,0 @@
1
- {
2
- "explorer.fileNesting.enabled": true,
3
- "explorer.fileNesting.patterns": {
4
- "tsconfig.json": "tsconfig.*.json, env.d.ts",
5
- "vite.config.*": "jsconfig*, vitest.config.*, cypress.config.*, playwright.config.*",
6
- "package.json": "package-lock.json, pnpm*, .yarnrc*, yarn*, .eslint*, eslint*, .oxlint*, oxlint*, .prettier*, prettier*, .editorconfig"
7
- },
8
- "editor.codeActionsOnSave": {
9
- "source.fixAll": "explicit"
10
- },
11
- "editor.formatOnSave": true,
12
- "editor.defaultFormatter": "esbenp.prettier-vscode",
13
- "[vue]": {
14
- "editor.defaultFormatter": "esbenp.prettier-vscode"
15
- }
16
- }