define([ 'jquery', 'underscore', 'annotator_1.2.9', 'logger', 'js/edxnotes/views/notes_factory', 'js/spec/edxnotes/custom_matchers' ], function($, _, Annotator, Logger, NotesFactory, customMatchers) { 'use strict'; describe('EdxNotes Accessibility Plugin', function() { function keyDownEvent (key) { return $.Event('keydown', {keyCode: key}); } function tabBackwardEvent () { return $.Event('keydown', {keyCode: $.ui.keyCode.TAB, shiftKey: true}); } function tabForwardEvent () { return $.Event('keydown', {keyCode: $.ui.keyCode.TAB, shiftKey: false}); } function enterMetaKeyEvent () { return $.Event('keydown', {keyCode: $.ui.keyCode.ENTER, metaKey: true}); } function enterControlKeyEvent () { return $.Event('keydown', {keyCode: $.ui.keyCode.ENTER, ctrlKey: true}); } beforeEach(function() { this.KEY = $.ui.keyCode; customMatchers(this); loadFixtures('js/fixtures/edxnotes/edxnotes_wrapper.html'); this.annotator = NotesFactory.factory( $('div#edx-notes-wrapper-123').get(0), { endpoint: 'http://example.com/' } ); this.plugin = this.annotator.plugins.Accessibility; spyOn(Logger, 'log'); }); afterEach(function () { _.invoke(Annotator._instances, 'destroy'); }); describe('destroy', function () { it('should unbind all events', function () { spyOn($.fn, 'off'); spyOn(this.annotator, 'unsubscribe').andCallThrough(); this.plugin.destroy(); expect(this.annotator.unsubscribe).toHaveBeenCalledWith( 'annotationViewerTextField', this.plugin.addAriaAttributes ); expect(this.annotator.unsubscribe).toHaveBeenCalledWith( 'annotationsLoaded', this.plugin.addDescriptions ); expect(this.annotator.unsubscribe).toHaveBeenCalledWith( 'annotationCreated', this.plugin.addDescriptions ); expect(this.annotator.unsubscribe).toHaveBeenCalledWith( 'annotationCreated', this.plugin.focusOnHighlightedText ); expect(this.annotator.unsubscribe).toHaveBeenCalledWith( 'annotationDeleted', this.plugin.removeDescription ); expect($.fn.off).toHaveBeenCalledWith('.accessibility'); }); }); describe('a11y attributes', function () { var highlight, annotation, note; beforeEach(function() { highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element); annotation = { id: '01', text: 'Test text', highlights: [highlight.get(0)] }; }); it('should be added to highlighted text and associated note', function () { this.annotator.viewer.load([annotation]); note = $('.annotator-note'); expect(note).toExist(); expect(note).toHaveAttr('tabindex', -1); expect(note).toHaveAttr('role', 'note'); expect(note).toHaveAttr('class', 'annotator-note'); }); it('should create aria-descriptions when annotations are loaded', function () { this.annotator.publish('annotationsLoaded', [[annotation]]); expect(highlight).toHaveAttr('aria-describedby', 'aria-note-description-01'); expect($('#aria-note-description-01')).toContainText('Test text'); }); it('should create aria-description when new annotation is created', function () { this.annotator.publish('annotationCreated', [annotation]); expect(highlight).toHaveAttr('aria-describedby', 'aria-note-description-01'); expect($('#aria-note-description-01')).toContainText('Test text'); }); it('should remove aria-description when the annotation is removed', function () { this.annotator.publish('annotationDeleted', [annotation]); expect($('#aria-note-description-01')).not.toExist(); }); }); describe('keydown events on highlighted text', function () { var highlight, annotation, note; beforeEach(function() { highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element); annotation = { id: '01', text: 'Test text', highlights: [highlight.get(0)] }; highlight.data('annotation', annotation); spyOn(this.annotator, 'showViewer').andCallThrough(); spyOn(this.annotator.viewer, 'hide').andCallThrough(); spyOn(this.plugin, 'focusOnGrabber').andCallThrough(); }); it('should open the viewer on SPACE keydown and focus on note', function () { highlight.trigger(keyDownEvent(this.KEY.SPACE)); expect(this.annotator.showViewer).toHaveBeenCalled(); }); it('should open the viewer on ENTER keydown and focus on note', function () { highlight.trigger(keyDownEvent(this.KEY.ENTER)); expect(this.annotator.showViewer).toHaveBeenCalled(); }); // This happens only when coming from notes page it('should open focus on viewer on TAB keydown if viewer is opened', function () { this.annotator.viewer.load([annotation]); highlight.trigger(keyDownEvent(this.KEY.TAB)); expect(this.annotator.element.find('.annotator-listing')).toBeFocused(); }); it('should focus highlighted text after closing', function () { var note; highlight.trigger(keyDownEvent(this.KEY.ENTER)); note = this.annotator.element.find('.annotator-edit'); note.trigger(keyDownEvent(this.KEY.ESCAPE)); expect(highlight).toBeFocused(); }); it('should focus on grabber after being deleted', function () { highlight.trigger(keyDownEvent(this.KEY.ENTER)); this.annotator.publish('annotationDeleted', {}); expect(this.plugin.focusGrabber).toBeFocused(); }); it('should not focus on grabber when the viewer is hidden', function () { this.annotator.publish('annotationDeleted', {}); expect(this.plugin.focusGrabber).not.toBeFocused(); }); }); describe('keydown events on viewer', function () { var highlight, annotation, listing, note, edit, del, close; beforeEach(function() { highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element); annotation = { id: '01', text: "Test text", highlights: [highlight.get(0)] }; highlight.data('annotation', annotation); this.annotator.viewer.load([annotation]); listing = this.annotator.element.find('.annotator-listing').first(); note = this.annotator.element.find('.annotator-note').first(); edit= this.annotator.element.find('.annotator-edit').first(); del = this.annotator.element.find('.annotator-delete').first(); close = this.annotator.element.find('.annotator-close').first(); spyOn(this.annotator.viewer, 'hide').andCallThrough(); }); it('should give focus to Note on Listing TAB keydown', function () { listing.focus(); listing.trigger(tabForwardEvent()); expect(note).toBeFocused(); }); it('should give focus to Close on Listing SHIFT + TAB keydown', function () { listing.focus(); listing.trigger(tabBackwardEvent()); expect(close).toBeFocused(); }); it('should cycle forward through Note, Edit, Delete, and Close on TAB keydown', function () { note.focus(); note.trigger(tabForwardEvent()); expect(edit).toBeFocused(); edit.trigger(tabForwardEvent()); expect(del).toBeFocused(); del.trigger(tabForwardEvent()); expect(close).toBeFocused(); close.trigger(tabForwardEvent()); expect(note).toBeFocused(); }); it('should cycle backward through Note, Edit, Delete, and Close on SHIFT + TAB keydown', function () { note.focus(); note.trigger(tabBackwardEvent()); expect(close).toBeFocused(); close.trigger(tabBackwardEvent()); expect(del).toBeFocused(); del.trigger(tabBackwardEvent()); expect(edit).toBeFocused(); edit.trigger(tabBackwardEvent()); expect(note).toBeFocused(); }); it('should hide on ESCAPE keydown', function () { var tabControls = [listing, note, edit, del, close]; _.each(tabControls, function (control) { control.focus(); control.trigger(keyDownEvent(this.KEY.ESCAPE)); }, this); expect(this.annotator.viewer.hide.callCount).toBe(5); }); }); describe('keydown events on editor', function () { var highlight, annotation, form, annotatorItems, textArea, tags, save, cancel; beforeEach(function() { highlight = $('<span class="annotator-hl" tabindex="0"/>').appendTo(this.annotator.element); annotation = { id: '01', text: "Test text", highlights: [highlight.get(0)] }; highlight.data('annotation', annotation); this.annotator.editor.show(annotation, {'left': 0, 'top': 0}); form = this.annotator.element.find('form.annotator-widget'); annotatorItems = this.annotator.element.find('.annotator-item'); textArea = annotatorItems.first().children('textarea'); tags = annotatorItems.first().next().children('input'); save = this.annotator.element.find('.annotator-save'); cancel = this.annotator.element.find('.annotator-cancel'); spyOn(this.annotator.editor, 'submit').andCallThrough(); spyOn(this.annotator.editor, 'hide').andCallThrough(); }); it('should give focus to TextArea on Form TAB keydown', function () { form.focus(); form.trigger(tabForwardEvent()); expect(textArea).toBeFocused(); }); it('should give focus to Cancel on Form SHIFT + TAB keydown', function () { form.focus(); form.trigger(tabBackwardEvent()); expect(cancel).toBeFocused(); }); it('should cycle forward through textarea, tags, save, and cancel on TAB keydown', function () { textArea.focus(); textArea.trigger(tabForwardEvent()); expect(tags).toBeFocused(); tags.trigger(tabForwardEvent()); expect(save).toBeFocused(); save.trigger(tabForwardEvent()); expect(cancel).toBeFocused(); cancel.trigger(tabForwardEvent()); expect(textArea).toBeFocused(); }); it('should cycle back through textarea, tags, save, and cancel on SHIFT + TAB keydown', function () { textArea.focus(); textArea.trigger(tabBackwardEvent()); expect(cancel).toBeFocused(); cancel.trigger(tabBackwardEvent()); expect(save).toBeFocused(); save.trigger(tabBackwardEvent()); expect(tags).toBeFocused(); tags.trigger(tabBackwardEvent()); expect(textArea).toBeFocused(); }); it('should submit if target is Save on ENTER or SPACE keydown', function () { save.focus(); save.trigger(keyDownEvent(this.KEY.ENTER)); expect(this.annotator.editor.submit).toHaveBeenCalled(); this.annotator.editor.submit.reset(); save.focus(); save.trigger(keyDownEvent(this.KEY.SPACE)); expect(this.annotator.editor.submit).toHaveBeenCalled(); }); it('should submit on META or CONTROL + ENTER keydown', function () { textArea.focus(); textArea.trigger(enterMetaKeyEvent()); expect(this.annotator.editor.submit).toHaveBeenCalled(); this.annotator.editor.submit.reset(); textArea.focus(); textArea.trigger(enterControlKeyEvent()); expect(this.annotator.editor.submit).toHaveBeenCalled(); }); it('should hide if target is Cancel on ENTER or SPACE keydown', function () { cancel.focus(); cancel.trigger(keyDownEvent(this.KEY.ENTER)); expect(this.annotator.editor.hide).toHaveBeenCalled(); this.annotator.editor.hide.reset(); cancel.focus(); save.trigger(keyDownEvent(this.KEY.SPACE)); expect(this.annotator.editor.hide).toHaveBeenCalled(); }); it('should hide on ESCAPE keydown', function () { var tabControls = [textArea, save, cancel]; _.each(tabControls, function (control) { control.focus(); control.trigger(keyDownEvent(this.KEY.ESCAPE)); }, this); expect(this.annotator.editor.hide.callCount).toBe(3); }); }); }); });