@@ -51,6 +51,19 @@ const copied = async (page: Page): Promise<string> => {
5151 return page . evaluate ( ( ) => navigator . clipboard . readText ( ) )
5252}
5353
54+ /**
55+ * Fills the source. It is edited in an editor, which loads apart and takes the textarea's place;
56+ * the textarea stays under it holding the same text, which is how the page reads the source. The
57+ * text is typed over a selection of all of it, made by the editor's own key: a page selection that
58+ * a test makes itself can be taken back by the editor, which keeps its own.
59+ */
60+ async function fillSource ( page : Page , text : string ) : Promise < void > {
61+ await page . locator ( '.source-editor .cm-content' ) . click ( )
62+ await page . keyboard . press ( 'ControlOrMeta+a' )
63+ await page . keyboard . insertText ( text )
64+ await expect ( page . locator ( '#source' ) ) . toHaveValue ( text )
65+ }
66+
5467const playerState = ( page : Page ) => page . locator ( 'h5p-player' ) . evaluate ( ( element : HTMLElement & { state ?: string } ) => element . state )
5568
5669async function expectPlays ( page : Page ) : Promise < void > {
@@ -67,7 +80,7 @@ test('a pasted source builds, plays and downloads', async ({ page, baseURL }) =>
6780 await expect ( page . locator ( '[data-theme-toggle]' ) ) . toBeVisible ( )
6881 await expect ( page . locator ( '#files-step' ) ) . toBeHidden ( )
6982 // A chat hands the source in a code fence; the page takes what is inside it.
70- await page . locator ( '#source' ) . fill ( '```json\n' + JSON . stringify ( RIVERS , null , 2 ) + '\n```' )
83+ await fillSource ( page , '```json\n' + JSON . stringify ( RIVERS , null , 2 ) + '\n```' )
7184 await page . locator ( '#build' ) . click ( )
7285 await expectPlays ( page )
7386 await expect ( page . locator ( '#status' ) ) . toContainText ( 'Built “Rivers”: H5P.Blanks, ' )
@@ -96,7 +109,7 @@ test('a source with problems gets them all listed, ready for the chat', async ({
96109 await context . grantPermissions ( [ 'clipboard-read' , 'clipboard-write' ] )
97110 await page . goto ( '/h5p/build/' )
98111 const { title : _title , ...untitled } = RIVERS
99- await page . locator ( '#source' ) . fill ( JSON . stringify ( { ...untitled , colour : 'blue' } ) )
112+ await fillSource ( page , JSON . stringify ( { ...untitled , colour : 'blue' } ) )
100113 await page . locator ( '#build' ) . click ( )
101114 await expect ( page . locator ( '#status' ) ) . toHaveAttribute ( 'data-state' , 'error' )
102115 const listed = page . locator ( '#problems li' )
@@ -115,7 +128,7 @@ test('a source with problems gets them all listed, ready for the chat', async ({
115128 expect ( files ) . toBe ( await typeFile ( request , 'H5P.Blanks' ) )
116129
117130 // Text that is not JSON is a problem of its own, said where it is, and names no content type.
118- await page . locator ( '#source' ) . fill ( '{ "type": "H5P.Blanks", ' )
131+ await fillSource ( page , '{ "type": "H5P.Blanks", ' )
119132 await page . locator ( '#build' ) . click ( )
120133 await expect ( listed ) . toHaveCount ( 1 )
121134 await expect ( listed . first ( ) ) . toContainText ( 'is not valid JSON' )
@@ -144,7 +157,7 @@ test('the copy carries the type files of the source’s type and of the librarie
144157 }
145158 }
146159 await page . goto ( '/h5p/build/' )
147- await page . locator ( '#source' ) . fill ( JSON . stringify ( book ) )
160+ await fillSource ( page , JSON . stringify ( book ) )
148161 await page . locator ( '#build' ) . click ( )
149162 await expect ( page . locator ( '#status' ) ) . toHaveAttribute ( 'data-state' , 'error' )
150163 // The build says what to write, as h5p-author does since 0.7.0.
@@ -156,7 +169,7 @@ test('the copy carries the type files of the source’s type and of the librarie
156169 expect ( files ) . toBe ( [ await typeFile ( request , 'H5P.InteractiveBook' ) , await typeFile ( request , 'H5P.AdvancedText' ) , await typeFile ( request , 'H5P.MultiChoice' ) ] . join ( '\n\n---\n\n' ) )
157170
158171 // A type the pack does not have has no file: its problems are copied alone.
159- await page . locator ( '#source' ) . fill ( JSON . stringify ( { ...book , type : 'H5P.ImageChoice' , params : { } } ) )
172+ await fillSource ( page , JSON . stringify ( { ...book , type : 'H5P.ImageChoice' , params : { } } ) )
160173 await page . locator ( '#build' ) . click ( )
161174 await expect ( page . locator ( '#problems li' ) ) . not . toHaveCount ( 0 )
162175 const alone = await copied ( page )
@@ -173,7 +186,7 @@ test('the copy carries the type file of a type named as it is spoken of, or unde
173186 /** The type files a source's copy carries after its problems, once a problem of it is shown. */
174187 const filesOf = async ( source : unknown , said : string ) : Promise < string | undefined > => {
175188 await page . goto ( '/h5p/build/' )
176- await page . locator ( '#source' ) . fill ( JSON . stringify ( source ) )
189+ await fillSource ( page , JSON . stringify ( source ) )
177190 await page . locator ( '#build' ) . click ( )
178191 await expect ( page . locator ( '#problems' ) ) . toContainText ( said )
179192 return ( await copied ( page ) ) . split ( `\n\n${ TYPES_HEADING } \n\n` ) [ 1 ]
@@ -209,7 +222,7 @@ test('the problems are copied alone where the type files cannot be fetched, and
209222 await page . route ( '**/h5p/build/types/**' , ( route ) => route . fulfill ( { status : 503 , body : 'unavailable' } ) )
210223 await page . goto ( '/h5p/build/' )
211224 const { title : _title , ...untitled } = RIVERS
212- await page . locator ( '#source' ) . fill ( JSON . stringify ( untitled ) )
225+ await fillSource ( page , JSON . stringify ( untitled ) )
213226 await page . locator ( '#build' ) . click ( )
214227 await expect ( page . locator ( '#problems li' ) ) . toHaveCount ( 1 )
215228 expect ( await copied ( page ) ) . toMatch ( / ^ T h e b u i l d p a g e f o u n d 1 p r o b l e m i n t h e s o u r c e \. F i x t h e m a l l a n d w r i t e t h e w h o l e s o u r c e a g a i n : \n - t i t l e : [ ^ \n ] * $ / )
@@ -231,11 +244,11 @@ test('type files that arrive once newer problems are shown are not copied with t
231244 } )
232245 await page . goto ( '/h5p/build/' )
233246 const { title : _title , ...untitled } = RIVERS
234- await page . locator ( '#source' ) . fill ( JSON . stringify ( untitled ) )
247+ await fillSource ( page , JSON . stringify ( untitled ) )
235248 await page . locator ( '#build' ) . click ( )
236249 await expect ( page . locator ( '#problems li code' ) ) . toHaveText ( 'title' )
237250 // Its type file is on its way when the next source's problems replace these: text that names no type.
238- await page . locator ( '#source' ) . fill ( '{ "type": ' )
251+ await fillSource ( page , '{ "type": ' )
239252 await page . locator ( '#build' ) . click ( )
240253 await expect ( page . locator ( '#problems li' ) ) . toContainText ( 'is not valid JSON' )
241254 const arrived = page . waitForResponse ( '**/h5p/build/types/H5P.Blanks.md' )
@@ -333,3 +346,111 @@ test('a source that names a file waits for it, then builds with it', async ({ pa
333346 await expect . poll ( ( ) => page . frameLocator ( 'h5p-player iframe' ) . first ( ) . locator ( 'img' ) . first ( ) . evaluate ( ( img : HTMLImageElement ) => img . naturalWidth ) ) . toBe ( 2 )
334347 expect ( problems , problems . join ( '\n' ) ) . toEqual ( [ ] )
335348} )
349+
350+ // The source's editor: CodeMirror, loaded apart, in place of the textarea, which stays under it.
351+
352+ test ( 'a mistake in the JSON is marked as it is typed, with its line and column, and goes when it is put right' , async ( { page, baseURL } ) => {
353+ const { problems } = watch ( page , baseURL )
354+ await page . goto ( '/h5p/build/' )
355+ const state = page . locator ( '#source-state' )
356+ await expect ( state ) . toHaveText ( '' )
357+ await fillSource ( page , '{\n "type": "H5P.Blanks"\n "title": "Rivers"\n}' )
358+ // Said in words, with where, in the line under the source and in the editor itself.
359+ await expect ( state ) . toContainText ( 'Not valid JSON at line 3, column 3: A comma is missing here' )
360+ await expect ( page . locator ( '.source-editor .cm-lintRange-error' ) ) . toHaveCount ( 1 )
361+ // The button goes where the mistake is.
362+ await state . getByRole ( 'button' , { name : 'Show where' } ) . click ( )
363+ await expect ( page . locator ( '.source-editor .cm-activeLine' ) ) . toContainText ( '"title": "Rivers"' )
364+ // Put right, it goes.
365+ await fillSource ( page , JSON . stringify ( RIVERS ) )
366+ await expect ( state ) . toHaveText ( '' )
367+ await expect ( page . locator ( '.source-editor .cm-lintRange-error' ) ) . toHaveCount ( 0 )
368+ expect ( problems , problems . join ( '\n' ) ) . toEqual ( [ ] )
369+ } )
370+
371+ test ( 'Format lays the source out, and takes it out of the code fence a chat put it in' , async ( { page, baseURL } ) => {
372+ const { problems } = watch ( page , baseURL )
373+ await page . goto ( '/h5p/build/' )
374+ await fillSource ( page , '```json\n' + JSON . stringify ( RIVERS ) + '\n```' )
375+ await page . locator ( '#format' ) . click ( )
376+ await expect ( page . locator ( '#source' ) ) . toHaveValue ( JSON . stringify ( RIVERS , null , 2 ) )
377+ await expect ( page . locator ( '#source-state' ) ) . toHaveText ( 'Laid out.' )
378+ await expect ( page . locator ( '.source-editor .cm-line' ) . first ( ) ) . toHaveText ( '{' )
379+ // Text that is not JSON is left as it is, and the line under the source says where.
380+ await fillSource ( page , '{ "type": ' )
381+ await page . locator ( '#format' ) . click ( )
382+ await expect ( page . locator ( '#source' ) ) . toHaveValue ( '{ "type": ' )
383+ await expect ( page . locator ( '#source-state' ) ) . toContainText ( 'The text ends before the JSON is complete' )
384+ expect ( problems , problems . join ( '\n' ) ) . toEqual ( [ ] )
385+ } )
386+
387+ test ( 'a problem the build finds is marked where it is in the source, and its name goes there' , async ( { page, baseURL } ) => {
388+ const { problems } = watch ( page , baseURL )
389+ await page . goto ( '/h5p/build/' )
390+ const { title : _title , ...untitled } = RIVERS
391+ await fillSource ( page , JSON . stringify ( { ...untitled , colour : 'blue' } , null , 2 ) )
392+ await page . locator ( '#build' ) . click ( )
393+ await expect ( page . locator ( '#status' ) ) . toHaveAttribute ( 'data-state' , 'error' )
394+ // The field that is not a field of a source, name and value; the one that is missing, in the object it is missing from.
395+ const marks = page . locator ( '.source-editor .cm-lintRange-error' )
396+ await expect ( marks ) . toHaveCount ( 2 )
397+ await expect ( marks . filter ( { hasText : '"colour": "blue"' } ) ) . toHaveCount ( 1 )
398+ // Each problem's name goes to its place.
399+ await page . locator ( '#problems button.jump' , { hasText : / ^ c o l o u r $ / } ) . click ( )
400+ await expect ( page . locator ( '.source-editor .cm-activeLine' ) ) . toContainText ( '"colour": "blue"' )
401+ await page . locator ( '#problems button.jump' , { hasText : / ^ t i t l e $ / } ) . click ( )
402+ await expect ( page . locator ( '.source-editor .cm-activeLine' ) ) . toHaveText ( '{' )
403+ // The marks are of the text as it was built: a change to it clears them.
404+ await page . keyboard . press ( 'ArrowRight' )
405+ await page . keyboard . type ( ' ' )
406+ await expect ( marks ) . toHaveCount ( 0 )
407+ // Text that is not JSON is a problem at a place of its own.
408+ await fillSource ( page , '{ "type": "H5P.Blanks" "title": 1 }' )
409+ await page . locator ( '#build' ) . click ( )
410+ await expect ( page . locator ( '#problems li' ) ) . toContainText ( 'is not valid JSON at line 1, column 24: A comma is missing here' )
411+ await page . locator ( '#problems button.jump' ) . click ( )
412+ await expect ( page . locator ( '.source-editor .cm-activeLine' ) ) . toContainText ( '"title": 1' )
413+ expect ( problems , problems . join ( '\n' ) ) . toEqual ( [ ] )
414+ } )
415+
416+ test ( 'the editor has a name, its label goes to it, and Tab leaves it' , async ( { page } ) => {
417+ await page . goto ( '/h5p/build/' )
418+ const editor = page . getByRole ( 'textbox' , { name : 'The source, as JSON' } )
419+ await expect ( editor ) . toBeVisible ( )
420+ await page . getByText ( 'The source, as JSON' , { exact : true } ) . click ( )
421+ await expect ( editor ) . toBeFocused ( )
422+ await page . keyboard . press ( 'Tab' )
423+ await expect ( page . locator ( '#format' ) ) . toBeFocused ( )
424+ } )
425+
426+ test ( 'text typed over a selection is the editor’s own, so a bracket wraps it and the policy is not asked' , async ( { page, baseURL } ) => {
427+ const { problems } = watch ( page , baseURL )
428+ await page . goto ( '/h5p/build/' )
429+ await fillSource ( page , JSON . stringify ( RIVERS , null , 2 ) )
430+ await page . locator ( '.source-editor .cm-content' ) . click ( )
431+ await page . keyboard . press ( 'ControlOrMeta+a' )
432+ await page . keyboard . type ( '[' )
433+ await expect ( page . locator ( '#source' ) ) . toHaveValue ( `[${ JSON . stringify ( RIVERS , null , 2 ) } ]` )
434+ await page . keyboard . press ( 'ControlOrMeta+a' )
435+ await page . keyboard . type ( 'x' )
436+ await expect ( page . locator ( '#source' ) ) . toHaveValue ( 'x' )
437+ await page . keyboard . press ( 'ControlOrMeta+z' )
438+ await expect ( page . locator ( '#source' ) ) . toHaveValue ( `[${ JSON . stringify ( RIVERS , null , 2 ) } ]` )
439+ expect ( problems , problems . join ( '\n' ) ) . toEqual ( [ ] )
440+ } )
441+
442+ test ( 'where the editor cannot load, the source is the textarea, and the page works' , async ( { page, baseURL } ) => {
443+ const { elsewhere } = watch ( page , baseURL )
444+ await page . route ( '**/h5p/assets/editor-*.js' , ( route ) => route . abort ( ) )
445+ await page . goto ( '/h5p/build/' )
446+ const field = page . locator ( '#source' )
447+ await expect ( field ) . toBeVisible ( )
448+ await field . fill ( JSON . stringify ( { ...RIVERS , authors : [ ] } ) )
449+ await page . locator ( '#build' ) . click ( )
450+ await expect ( page . locator ( '#problems li code' ) ) . toHaveText ( 'authors' )
451+ // Its name selects where it is, in the textarea.
452+ await page . locator ( '#problems button.jump' ) . click ( )
453+ expect ( await field . evaluate ( ( area : HTMLTextAreaElement ) => area . value . slice ( area . selectionStart , area . selectionEnd ) ) ) . toBe ( '"authors":[' )
454+ await expect ( page . locator ( '.source-editor' ) ) . toHaveCount ( 0 )
455+ expect ( elsewhere ) . toEqual ( [ ] )
456+ } )
0 commit comments