Commit cca16c88 authored by Robert Knight's avatar Robert Knight

Remove book_as_single_document flag and throw if book ID is missing

 - Remove the book_as_single_document flag, as we have now enabled this
   flag for everyone in production

 - Throw if the `isbn` property is missing from the book info returned
   by VitalSource. If this ever happens, we want to fail loudly instead
   of silently capturing a URL with no book ID on it.
parent 99b816c5
...@@ -11,11 +11,7 @@ import { warnOnce } from '../shared/warn-once'; ...@@ -11,11 +11,7 @@ import { warnOnce } from '../shared/warn-once';
* *
* @type {string[]} * @type {string[]}
*/ */
const annotatorFlags = [ const annotatorFlags = ['html_side_by_side', 'styled_highlight_clusters'];
'book_as_single_document',
'html_side_by_side',
'styled_highlight_clusters',
];
/** /**
* An observable container of feature flags. * An observable container of feature flags.
......
...@@ -25,9 +25,7 @@ export function createIntegration(annotator) { ...@@ -25,9 +25,7 @@ export function createIntegration(annotator) {
const vsFrameRole = vitalSourceFrameRole(); const vsFrameRole = vitalSourceFrameRole();
if (vsFrameRole === 'content') { if (vsFrameRole === 'content') {
return new VitalSourceContentIntegration(document.body, { return new VitalSourceContentIntegration(document.body);
features: annotator.features,
});
} }
return new HTMLIntegration({ features: annotator.features }); return new HTMLIntegration({ features: annotator.features });
......
import { delay, waitFor } from '../../../test-util/wait'; import { delay, waitFor } from '../../../test-util/wait';
import { FeatureFlags } from '../../features';
import { import {
VitalSourceInjector, VitalSourceInjector,
VitalSourceContentIntegration, VitalSourceContentIntegration,
...@@ -59,14 +58,12 @@ function resolveURL(relativeURL) { ...@@ -59,14 +58,12 @@ function resolveURL(relativeURL) {
} }
describe('annotator/integrations/vitalsource', () => { describe('annotator/integrations/vitalsource', () => {
let featureFlags;
let fakeViewer; let fakeViewer;
let FakeHTMLIntegration; let FakeHTMLIntegration;
let fakeHTMLIntegration; let fakeHTMLIntegration;
let fakeInjectClient; let fakeInjectClient;
beforeEach(() => { beforeEach(() => {
featureFlags = new FeatureFlags();
fakeViewer = new FakeVitalSourceViewer(); fakeViewer = new FakeVitalSourceViewer();
fakeHTMLIntegration = { fakeHTMLIntegration = {
...@@ -281,7 +278,6 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -281,7 +278,6 @@ describe('annotator/integrations/vitalsource', () => {
function createIntegration() { function createIntegration() {
const integration = new VitalSourceContentIntegration(document.body, { const integration = new VitalSourceContentIntegration(document.body, {
features: featureFlags,
bookElement: fakeBookElement, bookElement: fakeBookElement,
}); });
integrations.push(integration); integrations.push(integration);
...@@ -305,11 +301,6 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -305,11 +301,6 @@ describe('annotator/integrations/vitalsource', () => {
assert.calledWith(fakeHTMLIntegration.getAnnotatableRange, range); assert.calledWith(fakeHTMLIntegration.getAnnotatableRange, range);
}); });
it('asks guest to wait for feature flags before sending document info', () => {
const integration = createIntegration();
assert.isTrue(integration.waitForFeatureFlags());
});
it('asks sidebar to persist annotations after frame unloads', () => { it('asks sidebar to persist annotations after frame unloads', () => {
const integration = createIntegration(); const integration = createIntegration();
assert.isTrue(integration.persistFrame()); assert.isTrue(integration.persistFrame());
...@@ -415,20 +406,6 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -415,20 +406,6 @@ describe('annotator/integrations/vitalsource', () => {
}); });
describe('#getMetadata', () => { describe('#getMetadata', () => {
context('when "book_as_single_document" flag is off', () => {
it('returns metadata for current page/chapter', async () => {
const integration = createIntegration();
const metadata = await integration.getMetadata();
assert.equal(metadata.title, document.title);
assert.deepEqual(metadata.link, []);
});
});
context('when "book_as_single_document" flag is on', () => {
beforeEach(() => {
featureFlags.update({ book_as_single_document: true });
});
it('returns book metadata', async () => { it('returns book metadata', async () => {
const integration = createIntegration(); const integration = createIntegration();
const metadata = await integration.getMetadata(); const metadata = await integration.getMetadata();
...@@ -436,7 +413,6 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -436,7 +413,6 @@ describe('annotator/integrations/vitalsource', () => {
assert.deepEqual(metadata.link, []); assert.deepEqual(metadata.link, []);
}); });
}); });
});
describe('#navigateToSegment', () => { describe('#navigateToSegment', () => {
function createAnnotationWithSelector(selector) { function createAnnotationWithSelector(selector) {
...@@ -526,23 +502,7 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -526,23 +502,7 @@ describe('annotator/integrations/vitalsource', () => {
await urlChanged; await urlChanged;
}); });
context('when "book_as_single_document" flag is off', () => {
it('returns book chapter URL excluding query string', async () => {
const integration = createIntegration();
const uri = await integration.uri();
const parsedURL = new URL(uri);
assert.equal(parsedURL.hostname, document.location.hostname);
assert.equal(
parsedURL.pathname,
'/books/abc/epub/OPS/xhtml/chapter_001.html'
);
assert.equal(parsedURL.search, '');
});
});
context('when "book_as_single_document" flag is on', () => {
it('returns book reader URL', async () => { it('returns book reader URL', async () => {
featureFlags.update({ book_as_single_document: true });
const integration = createIntegration(); const integration = createIntegration();
const uri = await integration.uri(); const uri = await integration.uri();
assert.equal( assert.equal(
...@@ -550,19 +510,24 @@ describe('annotator/integrations/vitalsource', () => { ...@@ -550,19 +510,24 @@ describe('annotator/integrations/vitalsource', () => {
'https://bookshelf.vitalsource.com/reader/books/TEST-BOOK-ID' 'https://bookshelf.vitalsource.com/reader/books/TEST-BOOK-ID'
); );
}); });
it('throws if book ID is missing', async () => {
fakeBookElement.getBookInfo = () => ({
format: 'epub',
title: 'Test book title',
// `isbn` field is missing
}); });
it('updates when "book_as_single_document" flag changes', async () => {
const uriChanged = sinon.stub();
const integration = createIntegration(); const integration = createIntegration();
integration.on('uriChanged', uriChanged); let error;
const uri1 = await integration.uri(); try {
await integration.uri();
featureFlags.update({ book_as_single_document: true }); } catch (err) {
error = err;
}
assert.calledOnce(uriChanged); assert.instanceOf(error, Error);
const uri2 = await integration.uri(); assert.equal(error.message, 'Unable to get book ID from VitalSource');
assert.notEqual(uri1, uri2);
}); });
}); });
......
...@@ -12,7 +12,6 @@ import { injectClient } from '../hypothesis-injector'; ...@@ -12,7 +12,6 @@ import { injectClient } from '../hypothesis-injector';
import type { import type {
Anchor, Anchor,
AnnotationData, AnnotationData,
FeatureFlags as IFeatureFlags,
Integration, Integration,
SegmentInfo, SegmentInfo,
SidebarLayout, SidebarLayout,
...@@ -204,7 +203,6 @@ export class VitalSourceContentIntegration ...@@ -204,7 +203,6 @@ export class VitalSourceContentIntegration
implements Integration implements Integration
{ {
private _bookElement: MosaicBookElement; private _bookElement: MosaicBookElement;
private _features: IFeatureFlags;
private _htmlIntegration: HTMLIntegration; private _htmlIntegration: HTMLIntegration;
private _listeners: ListenerCollection; private _listeners: ListenerCollection;
private _textLayer?: ImageTextLayer; private _textLayer?: ImageTextLayer;
...@@ -212,16 +210,14 @@ export class VitalSourceContentIntegration ...@@ -212,16 +210,14 @@ export class VitalSourceContentIntegration
constructor( constructor(
/* istanbul ignore next - defaults are overridden in tests */ /* istanbul ignore next - defaults are overridden in tests */
container: HTMLElement = document.body, container: HTMLElement = document.body,
/* istanbul ignore next - defaults are overridden in tests */
options: { options: {
features: IFeatureFlags;
// Test seam // Test seam
bookElement?: MosaicBookElement; bookElement?: MosaicBookElement;
} } = {}
) { ) {
super(); super();
this._features = options.features;
const bookElement = const bookElement =
options.bookElement ?? findBookElement(window.parent.document); options.bookElement ?? findBookElement(window.parent.document);
if (!bookElement) { if (!bookElement) {
...@@ -232,12 +228,6 @@ export class VitalSourceContentIntegration ...@@ -232,12 +228,6 @@ export class VitalSourceContentIntegration
} }
this._bookElement = bookElement; this._bookElement = bookElement;
// If the book_as_single_document flag changed, this will change the
// document URI returned by this integration.
this._features.on('flagsChanged', () => {
this.emit('uriChanged');
});
const htmlFeatures = new FeatureFlags(); const htmlFeatures = new FeatureFlags();
// Forcibly enable the side-by-side feature for VS books. This feature is // Forcibly enable the side-by-side feature for VS books. This feature is
...@@ -401,7 +391,6 @@ export class VitalSourceContentIntegration ...@@ -401,7 +391,6 @@ export class VitalSourceContentIntegration
} }
async getMetadata() { async getMetadata() {
if (this._bookIsSingleDocument()) {
const bookInfo = this._bookElement.getBookInfo(); const bookInfo = this._bookElement.getBookInfo();
return { return {
title: bookInfo.title, title: bookInfo.title,
...@@ -409,14 +398,6 @@ export class VitalSourceContentIntegration ...@@ -409,14 +398,6 @@ export class VitalSourceContentIntegration
}; };
} }
// Return minimal metadata which includes only the information we really
// want to include.
return {
title: document.title,
link: [],
};
}
navigateToSegment(ann: AnnotationData) { navigateToSegment(ann: AnnotationData) {
const selector = ann.target[0].selector?.find( const selector = ann.target[0].selector?.find(
s => s.type === 'EPUBContentSelector' s => s.type === 'EPUBContentSelector'
...@@ -484,49 +465,15 @@ export class VitalSourceContentIntegration ...@@ -484,49 +465,15 @@ export class VitalSourceContentIntegration
} }
async uri() { async uri() {
if (this._bookIsSingleDocument()) {
const bookInfo = this._bookElement.getBookInfo(); const bookInfo = this._bookElement.getBookInfo();
const bookId = bookInfo.isbn; const bookId = bookInfo.isbn;
return `https://bookshelf.vitalsource.com/reader/books/${bookId}`; if (!bookId) {
throw new Error('Unable to get book ID from VitalSource');
} }
return `https://bookshelf.vitalsource.com/reader/books/${bookId}`;
// An example of a typical URL for the chapter content in the Bookshelf reader is:
//
// https://jigsaw.vitalsource.com/books/9781848317703/epub/OPS/xhtml/chapter_001.html#cfi=/6/10%5B;vnd.vst.idref=chap001%5D!/4
//
// Where "9781848317703" is the VitalSource book ID ("vbid"), "chapter_001.html"
// is the location of the HTML page for the current chapter within the book
// and the `#cfi` fragment identifies the scroll location.
//
// Note that this URL is typically different than what is displayed in the
// iframe's `src` attribute.
// Strip off search parameters and fragments.
const uri = new URL(document.location.href);
uri.search = '';
return uri.toString();
} }
async scrollToAnchor(anchor: Anchor) { async scrollToAnchor(anchor: Anchor) {
return this._htmlIntegration.scrollToAnchor(anchor); return this._htmlIntegration.scrollToAnchor(anchor);
} }
/**
* Return true if the feature flag to treat books as one document is enabled,
* as opposed to treating each chapter/segment/page as a separate document.
*/
_bookIsSingleDocument(): boolean {
return this._features.flagEnabled('book_as_single_document');
}
waitForFeatureFlags() {
// The `book_as_single_document` flag changes the URI reported by this
// integration.
//
// Ask the guest to delay reporting document metadata to the sidebar until
// feature flags have been received. This ensures that the initial document
// info reported to the sidebar after a chapter navigation is consistent
// between the previous/new guest frames.
return true;
}
} }
...@@ -1451,7 +1451,7 @@ describe('Guest', () => { ...@@ -1451,7 +1451,7 @@ describe('Guest', () => {
assert.isFalse(sidebarRPC().call.calledWith('documentInfoChanged')); assert.isFalse(sidebarRPC().call.calledWith('documentInfoChanged'));
emitSidebarEvent('featureFlagsUpdated', { emitSidebarEvent('featureFlagsUpdated', {
book_as_single_document: true, some_new_feature: true,
}); });
await delay(0); await delay(0);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment