Repository: learningequality/kolibri

Issue:
Epub render - does not split tables and font resizing issue
### Observed behavior
<!--
Description of the behavior that was observed, including screenshots or other references when applicable
-->

1. EPUB files containing tables do not split-render the tables.
2. Font resizing does not work.

Problem can be seen in this capture -

![arryfqrrpl](https://user-images.githubusercontent.com/2577060/51668624-a43f5b00-1fe8-11e9-93ef-a70de6f46230.gif)

### Expected behavior
<!--
Description of what behavior was expected but did not occur
-->

- Table should be split over multiple pages.
- Font resizing should work 

Expected behaviour shown in the capture herein from Microsoft edge -

![smvgozehpn](https://user-images.githubusercontent.com/2577060/51668702-cf29af00-1fe8-11e9-9f75-89c1c0ee6a3e.gif)

### User-facing consequences
<!--
Implications and real-world consequences for learners, coaches, admins, and other users of the application
-->

- Content not accessible to users when part of tables are hidden from view.
- Inconvenience for readers who may font a bigger or smaller font size.

### Errors and logs
<!--
Relevant logs from:
 * the command line
 * ~/.kolibri/kolibri.log
 * the browser console

Please wrap errors in triple backticks for clean formatting like this:
```
01:10 info: something happened
01:12 error: something bad happened
```
-->

NA

### Steps to reproduce
<!--
Precise steps that someone else can follow in order to see this behavior
-->

Upload the attached epub into a studio channel and take it offline in Kolibri app.

### Context
<!--
Tell us about your environment, including:
 * Kolibri version
 * Operating system
 * Browser
-->

 * Kolibri version - 0.11.0



Discussion:

Resolving patch:
diff --git a/kolibri/core/content/management/commands/scanforcontent.py b/kolibri/core/content/management/commands/scanforcontent.py
--- a/kolibri/core/content/management/commands/scanforcontent.py
+++ b/kolibri/core/content/management/commands/scanforcontent.py
@@ -1,6 +1,7 @@
 import logging
 
 from django.core.management.base import BaseCommand
+from sqlalchemy.exc import DatabaseError
 
 from ...utils.annotation import annotate_content
 from ...utils.channel_import import FutureSchemaError
@@ -41,5 +42,11 @@ def handle(self, *args, **options):
                             channel_id=channel_id
                         )
                     )
+                except DatabaseError:
+                    logger.warning(
+                        "Tried to import channel {channel_id}, but database file was corrupted.".format(
+                            channel_id=channel_id
+                        )
+                    )
             else:
                 annotate_content(channel_id)


Test patch:
diff --git a/integration_testing/features/learner/learner-engage-content-recommended.feature b/integration_testing/features/learner/learner-engage-content-recommended.feature
--- a/integration_testing/features/learner/learner-engage-content-recommended.feature
+++ b/integration_testing/features/learner/learner-engage-content-recommended.feature
@@ -18,10 +18,10 @@ Feature: Learner engages recommended content
     Then I see the *Next resource* heading and content item under the player
       And I see the *Next* snackbar alert with the title of the next recommended content item
       And I see the *+500 points* snackbar alert
-      And I see my points couter is inreased by 500 
+      And I see my points counter is increased by 500
     When I click on *Recommended* in the breadcrumb above the player
     Then I see the *Learn > Recommended* page
 
 Examples:
   | channel      | content_item                     |
-  | Khan Academy | Intro to springs and Hooke's law |
\ No newline at end of file
+  | Khan Academy | Intro to springs and Hooke's law |
diff --git a/integration_testing/features/learner/learner-engage-epub-content.feature b/integration_testing/features/learner/learner-engage-epub-content.feature
new file mode 100644
--- /dev/null
+++ b/integration_testing/features/learner/learner-engage-epub-content.feature
@@ -0,0 +1,99 @@
+Feature: Learner engages ePUB content
+  Learner needs to be able to engage with ePUB content and use all the ePUB reader features
+
+  Background:
+    Given I am signed in to Kolibri as a Learner user
+      And there are one or more channels imported on the device with ePUB content
+      And I am on the *Channels* page for a channel with ePUB content
+
+    Scenario: Browse and find ePUB content
+      When I am on the *Channels* page for <channel>
+      Then I see the *Channels > '<channel>'* breadcrumb
+        And I see all the topics for the channel <channel>
+      When I click the topic <topic>
+      Then I see the *Channels > '<channel>' > '<topic>'* breadcrumb
+        And I see all the the subtopics and resources of the topic <topic>
+        And I recognize <resource> resource as an ePUB document by the content type icon in the upper left corner
+
+    Scenario: Open paginated ePUB
+      Given that <resource> resource does not contain tables
+        When I click the <resource> resource
+        Then I see the *Channels > '<channel>' > '<topic>' > '<resource>'* breadcrumb
+          And I see the <resource> content
+            But I cannot scroll vertically to see the next page
+        When I click the right arrow button to go to next page
+        Then I see the next page of the <resource>
+        When I click the left arrow button to go to previous page
+        Then I see the previous page of the <resource>
+
+    Scenario: Open ePUB with tables
+      Given that <resource> resource contains tables
+        When I click the <resource> resource
+        Then I see the *Channels > '<channel>' > '<topic>' > '<resource>'* breadcrumb
+          And I see the <resource> content
+          And I am able to scroll vertically the <resource> content
+            But I do not see left and right arrow buttons to go to next and previous page
+
+    Scenario: Engage with ePUB table of contents
+      When I click the table of content button in the upper left corner 
+      Then I see the book's table of contents overlaid on the left side
+      When I click table of content button again
+      Then I see the book's table of contents disappear
+      When I click the button to *Toggle table of contents side bar*
+      Then I see the book's table of contents overlaid on the left side of the content again
+      When I click on a heading inside the table of contents
+      Then I see the table of contents side bar disappear
+        And I see the <resource> content change to the chosen heading
+
+    Scenario: Engage with EPUB settings
+      When I click the settings button
+      Then I see the settings sidebar overlaid on the right side of the content
+        And options for *Text Size* and *Theme* 
+      When I click the button to *Increase text size*
+      Then I see the text size of <resource> become larger
+      When I click the button to *Decrease text size*
+      Then I see the text size of <resource> become smaller
+        And I see the text size as it was originally
+      When I click the button to *Set beige theme*
+      Then I see the background of <resource> become beige
+        And I see the text color of <resource> become brown
+      When I click the button to *Set grey theme*
+      Then I see the background of <resource> become grey
+        And I see the text color of <resource> become white
+      When I click the button to *Set black theme*
+      Then I see the background of <resource> become black
+        And I see the text color of <resource> become white
+      When I click the button to *Set high contrast black theme*
+      Then I see the background of <resource> become black
+        And I see the text color of <resource> become yellow
+      When I click the button to *Set high contrast white theme*
+      Then I see the background of <resource> become white
+        And I see the text color of <resource> become blue
+      When I click the button to *Set white theme*
+      Then I see the background of <resource> become white
+        And I see the text color of <resource> become black
+      When I click the settings button again
+      Then I see the settings sidebar disappear
+
+    Scenario: Engages with EPUB search
+      When I click the search button
+      Then I see the search sidebar overlaid on the right side of the content
+      When I type my <search> term into the field
+        And press Enter key or click the button after the field
+      Then I see search results below the field
+        And I see <search> term highlighted in the ePUB content
+      When I click the search button again
+      Then I see the search sidebar disappear
+        And I see the <search> term is no longer highlighted in the ePUB content
+
+    Scenario: Engages with EPUB fullscreen mode
+      When I click the button for fullscreen 
+      Then I see ePUB viewer expands fullscreen
+      When I click the button to exit fullscreen or I press the Esc key
+      Then I see ePUB viewer as before
+
+
+Examples:
+  | channel              | topic            | resource                          | search |
+  | EPub Testing Channel | Richard's EPubs  | The Adventures of Sherlock Holmes | home   |
+  | EPub Testing Channel | Blaine's EPubs   | Epub with Tables!                 | street |
diff --git a/kolibri/plugins/coach/assets/test/views/AssignmentDeleteModal.spec.js b/kolibri/plugins/coach/assets/test/views/AssignmentDeleteModal.spec.js
--- a/kolibri/plugins/coach/assets/test/views/AssignmentDeleteModal.spec.js
+++ b/kolibri/plugins/coach/assets/test/views/AssignmentDeleteModal.spec.js
@@ -10,7 +10,6 @@ const defaultProps = {
 function makeWrapper(options) {
   const wrapper = mount(AssignmentDeleteModal, options);
   const els = {
-    submitButton: () => wrapper.find('button[type="submit"]'),
     cancelButton: () => wrapper.find('button[type="button"]'),
     form: () => wrapper.find('form'),
   };
@@ -18,23 +17,29 @@ function makeWrapper(options) {
 }
 
 describe('AssignmentDeleteModal', () => {
-  it('clicking delete causes a "delete" event to be emitted', () => {
-    const { wrapper, els } = makeWrapper({
+  it('clicking delete causes a "submit" event to be emitted', () => {
+    const submitListener = jest.fn();
+    const { els } = makeWrapper({
       propsData: { ...defaultProps },
       store,
+      listeners: {
+        submit: submitListener,
+      },
     });
-    // Again, clicking the submit button does not propagate to form, so doing a hack
-    // els.submitButton().trigger('click');
     els.form().trigger('submit');
-    expect(wrapper.emitted().submit.length).toEqual(1);
+    expect(submitListener).toHaveBeenCalled();
   });
 
   it('clicking cancel causes a "cancel" event to be emitted', () => {
-    const { wrapper, els } = makeWrapper({
+    const cancelListener = jest.fn();
+    const { els } = makeWrapper({
       propsData: { ...defaultProps },
       store,
+      listeners: {
+        cancel: cancelListener,
+      },
     });
     els.cancelButton().trigger('click');
-    expect(wrapper.emitted().cancel.length).toEqual(1);
+    expect(cancelListener).toHaveBeenCalled();
   });
 });
diff --git a/kolibri/plugins/device_management/assets/test/views/channel-token-modal.spec.js b/kolibri/plugins/device_management/assets/test/views/channel-token-modal.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/channel-token-modal.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/channel-token-modal.spec.js
@@ -2,8 +2,8 @@ import { mount } from '@vue/test-utils';
 import store from 'kolibri.coreVue.vuex.store';
 import ChannelTokenModal from '../../src/views/AvailableChannelsPage/ChannelTokenModal';
 
-function makeWrapper() {
-  return mount(ChannelTokenModal, { store });
+function makeWrapper(options = {}) {
+  return mount(ChannelTokenModal, { store, ...options });
 }
 
 function getElements(wrapper) {
@@ -25,21 +25,24 @@ describe('channelTokenModal component', () => {
     wrapper = makeWrapper();
   });
 
-  it('pressing "cancel" emits a close modal event', () => {
+  it('pressing "cancel" emits a "cancel" event', () => {
+    const cancelListener = jest.fn();
+    wrapper = makeWrapper({
+      listeners: {
+        cancel: cancelListener,
+      },
+    });
     const { cancelButton } = getElements(wrapper);
     cancelButton().trigger('click');
-    return wrapper.vm.$nextTick().then(() => {
-      expect(wrapper.emitted().closemodal.length).toEqual(1);
-    });
+    expect(cancelListener).toHaveBeenCalled();
   });
 
   describe('submitting a token', () => {
-    function inputToken(wrapper, token) {
+    async function inputToken(wrapper, token) {
       const textbox = getElements(wrapper).tokenTextbox();
       textbox.vm.$emit('input', token);
-      return wrapper.vm.$nextTick().then(() => {
-        expect(textbox.props().value).toEqual(token.trim());
-      });
+      await wrapper.vm.$nextTick();
+      expect(textbox.props().value).toEqual(token.trim());
     }
 
     function assertTextboxInvalid(wrapper) {
@@ -72,7 +75,7 @@ describe('channelTokenModal component', () => {
         });
     });
 
-    it('emits a "channel found" event  if token lookup is successful', () => {
+    it('emits a "submit" event if token lookup is successful', () => {
       const tokenPayload = { id: 'toka-toka-token' };
       const { lookupTokenStub } = getElements(wrapper);
       const lookupStub = lookupTokenStub();
@@ -83,7 +86,7 @@ describe('channelTokenModal component', () => {
         })
         .then(() => {
           expect(lookupStub).toHaveBeenCalledWith('toka-toka-token');
-          expect(wrapper.emitted().channelfound).toEqual([[tokenPayload]]);
+          expect(wrapper.emitted().submit).toEqual([[tokenPayload]]);
         });
     });
 
@@ -99,51 +102,39 @@ describe('channelTokenModal component', () => {
         });
     });
 
-    it('on blur, shows a validation message when token code is empty', () => {
+    it('on blur, shows a validation message when token code is empty', async () => {
       const textbox = getElements(wrapper).tokenTextbox();
-      return inputToken(wrapper, '    ')
-        .then(() => {
-          // Reaching into ui-textbox's blur to trigger it on k-textbox
-          textbox.vm.$refs.textbox.$emit('blur');
-          return wrapper.vm.$nextTick();
-        })
-        .then(() => {
-          assertTextboxInvalid(wrapper);
-        });
+      await inputToken(wrapper, '    ');
+      // Reaching into ui-textbox's blur to trigger it on k-textbox
+      textbox.vm.$refs.textbox.$emit('blur');
+      await wrapper.vm.$nextTick();
+      assertTextboxInvalid(wrapper);
     });
 
-    it('if the token does not point to a channel (404 code), shows a validation message', () => {
+    it('if the token does not point to a channel (404 code), shows a validation message', async () => {
       const tokenPayload = { status: { code: 404 } };
       const { lookupTokenStub } = getElements(wrapper);
       const lookupStub = lookupTokenStub();
       lookupStub.mockRejectedValue(tokenPayload);
-      return inputToken(wrapper, 'toka-toka-token')
-        .then(() => {
-          return wrapper.vm.submitForm();
-        })
-        .then(() => {
-          expect(lookupStub).toHaveBeenCalledWith('toka-toka-token');
-          expect(wrapper.emittedByOrder().length).toEqual(0);
-          assertTextboxInvalid(wrapper);
-        });
+      await inputToken(wrapper, 'toka-toka-token');
+      await wrapper.vm.submitForm();
+      expect(lookupStub).toHaveBeenCalledWith('toka-toka-token');
+      expect(wrapper.emittedByOrder().length).toEqual(0);
+      assertTextboxInvalid(wrapper);
     });
 
-    it('shows an ui-alert error if there is a generic network error (other error code)', () => {
+    it('shows an ui-alert error if there is a generic network error (other error code)', async () => {
       const tokenPayload = { status: { code: 500 } };
       const { tokenTextbox, networkErrorAlert, lookupTokenStub } = getElements(wrapper);
       const textbox = tokenTextbox();
       const lookupStub = lookupTokenStub();
       lookupStub.mockRejectedValue(tokenPayload);
-      return inputToken(wrapper, 'toka-toka-token')
-        .then(() => {
-          return wrapper.vm.submitForm();
-        })
-        .then(() => {
-          expect(lookupStub).toHaveBeenCalledWith('toka-toka-token');
-          expect(wrapper.emittedByOrder().length).toEqual(0);
-          expect(textbox.props().invalid).toEqual(false);
-          expect(networkErrorAlert().isVueInstance()).toEqual(true);
-        });
+      await inputToken(wrapper, 'toka-toka-token');
+      await wrapper.vm.submitForm();
+      expect(lookupStub).toHaveBeenCalledWith('toka-toka-token');
+      expect(wrapper.emittedByOrder().length).toEqual(0);
+      expect(textbox.props().invalid).toEqual(false);
+      expect(networkErrorAlert().isVueInstance()).toEqual(true);
     });
   });
 });
diff --git a/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js b/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js
--- a/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js
+++ b/kolibri/plugins/device_management/assets/test/views/channels-grid.spec.js
@@ -23,7 +23,7 @@ function getElements(wrapper) {
     channelListItems: () => wrapper.findAll({ name: 'ChannelListItem' }),
     emptyState: () => wrapper.find('.no-channels'),
     ProgressBar: () => wrapper.find({ name: 'KLinearLoader' }),
-    deleteChannelModal: () => wrapper.find({ name: 'KModal' }),
+    deleteChannelModal: () => wrapper.find({ name: 'DeleteChannelModal' }),
   };
 }
 
diff --git a/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js b/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js
--- a/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js
+++ b/kolibri/plugins/facility_management/assets/test/views/facility-config-page.spec.js
@@ -19,7 +19,7 @@ function getElements(wrapper) {
     confirmResetButton: () => wrapper.find('button[name="submit"]'),
     resetButton: () => wrapper.find('button[name="reset-settings"]'),
     saveButton: () => wrapper.find('button[name="save-settings"]'),
-    confirmResetModal: () => wrapper.find({ name: 'KModal' }),
+    confirmResetModal: () => wrapper.find({ name: 'ConfirmResetModal' }),
     form: () => wrapper.find('form'),
   };
 }
diff --git a/kolibri/plugins/setup_wizard/assets/test/views/PersonalDataConsentForm.spec.js b/kolibri/plugins/setup_wizard/assets/test/views/PersonalDataConsentForm.spec.js
--- a/kolibri/plugins/setup_wizard/assets/test/views/PersonalDataConsentForm.spec.js
+++ b/kolibri/plugins/setup_wizard/assets/test/views/PersonalDataConsentForm.spec.js
@@ -10,7 +10,7 @@ function makeWrapper() {
 describe('PersonalDataConsentForm', () => {
   it('the "View statement" opens the statement', () => {
     const { wrapper } = makeWrapper();
-    const modal = () => wrapper.find({ name: 'KModal' });
+    const modal = () => wrapper.find({ name: 'PrivacyInfoModal' });
     const button = wrapper.find({ name: 'KButton' });
     expect(modal().exists()).toEqual(false);
     button.vm.$emit('click');