Unverified Commit a1c2b014 authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #924 from hypothesis/format-code-with-prettier

Format code with Prettier
parents 231ff644 c78308dd
......@@ -6,10 +6,7 @@
"rules": {
"mocha/no-exclusive-tests": "error",
"no-var": "error",
"indent": ["error", 2, {
"ArrayExpression": "first",
"ObjectExpression": "first"
}]
"indent": "off"
},
"parserOptions": {
"ecmaVersion": 2018
......
{
"singleQuote": true,
"trailingComma": "es5"
}
......@@ -10,7 +10,9 @@ matrix:
# See https://github.com/hypothesis/client/pull/27#discussion_r70611726
- env: ACTION=lint
node_js: '10'
script: yarn run lint
script:
- make checkformatting
- make lint
- env: ACTION=test
node_js: '10'
after_success:
......
......@@ -6,6 +6,8 @@ help:
@echo "make help Show this help message"
@echo "make dev Run the app in the development server"
@echo "make lint Run the code linter(s) and print any warnings"
@echo "make checkformatting Check code formatting"
@echo "make format Automatically format code"
@echo "make test Run the unit tests"
@echo "make docs Build docs website and serve it locally"
@echo "make checkdocs Crash if building the docs website fails"
......@@ -41,6 +43,14 @@ clean:
rm -f node_modules/.uptodate
rm -rf build
.PHONY: format
format:
yarn run format
.PHONY: checkformatting
checkformatting:
yarn run checkformatting
build/manifest.json: node_modules/.uptodate
yarn run build
......
......@@ -84,6 +84,7 @@
"npm-packlist": "^1.1.12",
"postcss": "^7.0.13",
"postcss-url": "^8.0.0",
"prettier": "1.16.4",
"proxyquire": "^2.1.0",
"proxyquire-universal": "^2.1.0",
"proxyquireify": "^3.2.1",
......@@ -145,6 +146,8 @@
"build": "cross-env NODE_ENV=production gulp build",
"deps": "check-dependencies",
"lint": "eslint .",
"checkformatting": "prettier --check 'src/**/*.js'",
"format": "prettier --write 'src/**/*.js'",
"test": "gulp test",
"report-coverage": "codecov -f coverage/coverage-final.json",
"version": "make clean build/manifest.json",
......
......@@ -2,12 +2,16 @@
module.exports = {
rules: {
'no-restricted-properties': [2, {
// Disable `bind` usage in annotator/ code to prevent unexpected behavior
// due to broken bind polyfills. See
// https://github.com/hypothesis/client/issues/245
property: 'bind',
message: 'Use function expressions instead of bind() in annotator/ code'
}]
}
'no-restricted-properties': [
2,
{
// Disable `bind` usage in annotator/ code to prevent unexpected behavior
// due to broken bind polyfills. See
// https://github.com/hypothesis/client/issues/245
property: 'bind',
message:
'Use function expressions instead of bind() in annotator/ code',
},
],
},
};
......@@ -39,7 +39,7 @@ const ARROW_H_MARGIN = 20;
function attachShadow(element) {
if (element.attachShadow) {
// Shadow DOM v1 (Chrome v53, Safari 10)
return element.attachShadow({mode: 'open'});
return element.attachShadow({ mode: 'open' });
} else if (element.createShadowRoot) {
// Shadow DOM v0 (Chrome ~35-52)
return element.createShadowRoot();
......@@ -85,11 +85,13 @@ function createAdderDOM(container) {
element = shadowRoot.querySelector('.js-adder');
// Load stylesheets required by adder into shadow DOM element
const adderStyles = Array.from(document.styleSheets).map(function (sheet) {
return sheet.href;
}).filter(function (url) {
return (url || '').match(/(icomoon|annotator)\.css/);
});
const adderStyles = Array.from(document.styleSheets)
.map(function(sheet) {
return sheet.href;
})
.filter(function(url) {
return (url || '').match(/(icomoon|annotator)\.css/);
});
// Stylesheet <link> elements are inert inside shadow roots [1]. Until
// Shadow DOM implementations support external stylesheets [2], grab the
......@@ -103,9 +105,11 @@ function createAdderDOM(container) {
// get a usable adder, albeit one that uses browser default styles for the
// toolbar.
const styleEl = document.createElement('style');
styleEl.textContent = adderStyles.map(function (url) {
return '@import "' + url + '";';
}).join('\n');
styleEl.textContent = adderStyles
.map(function(url) {
return '@import "' + url + '";';
})
.join('\n');
shadowRoot.appendChild(styleEl);
} else {
container.innerHTML = template;
......@@ -161,10 +165,16 @@ class Adder {
this.hide();
};
this.element.querySelector(ANNOTATE_BTN_SELECTOR)
.addEventListener('click', event => handleCommand(event, options.onAnnotate));
this.element.querySelector(HIGHLIGHT_BTN_SELECTOR)
.addEventListener('click', event => handleCommand(event, options.onHighlight));
this.element
.querySelector(ANNOTATE_BTN_SELECTOR)
.addEventListener('click', event =>
handleCommand(event, options.onAnnotate)
);
this.element
.querySelector(HIGHLIGHT_BTN_SELECTOR)
.addEventListener('click', event =>
handleCommand(event, options.onHighlight)
);
this._width = () => this.element.getBoundingClientRect().width;
this._height = () => this.element.getBoundingClientRect().height;
......@@ -173,7 +183,7 @@ class Adder {
/** Hide the adder */
hide() {
clearTimeout(this._enterTimeout);
this.element.className = classnames({'annotator-adder': true});
this.element.className = classnames({ 'annotator-adder': true });
this.element.style.visibility = 'hidden';
}
......@@ -211,8 +221,10 @@ class Adder {
// Flip arrow direction if adder would appear above the top or below the
// bottom of the viewport.
if (targetRect.top - this._height() < 0 &&
arrowDirection === ARROW_POINTING_DOWN) {
if (
targetRect.top - this._height() < 0 &&
arrowDirection === ARROW_POINTING_DOWN
) {
arrowDirection = ARROW_POINTING_UP;
} else if (targetRect.top + this._height() > this._view.innerHeight) {
arrowDirection = ARROW_POINTING_DOWN;
......@@ -231,7 +243,7 @@ class Adder {
top = Math.max(top, 0);
top = Math.min(top, this._view.innerHeight - this._height());
return {top, left, arrowDirection};
return { top, left, arrowDirection };
}
/**
......
......@@ -72,8 +72,8 @@ function getPageTextContent(pageIndex) {
return textContent;
};
pageTextCache[pageIndex] = getPage(pageIndex).pdfPage
.getTextContent({
pageTextCache[pageIndex] = getPage(pageIndex)
.pdfPage.getTextContent({
normalizeWhitespace: true,
})
.then(joinItems);
......@@ -261,13 +261,15 @@ function findInPages(pageIndexes, quoteSelector, positionHint) {
};
// First, get the text offset and other details of the current page.
return Promise.all([page, content, offset])
// Attempt to locate the quote in the current page.
.then(attempt)
// If the quote is located, find the DOM range and return it.
.then(cacheAndFinish)
// If the quote was not found, try the next page.
.catch(next);
return (
Promise.all([page, content, offset])
// Attempt to locate the quote in the current page.
.then(attempt)
// If the quote is located, find the DOM range and return it.
.then(cacheAndFinish)
// If the quote was not found, try the next page.
.catch(next)
);
}
/**
......
......@@ -33,7 +33,7 @@ function createPage(content, rendered) {
const textLayer = document.createElement('div');
textLayer.classList.add('textLayer');
content.split(/\n/).forEach((item) => {
content.split(/\n/).forEach(item => {
const itemEl = document.createElement('div');
itemEl.textContent = item;
textLayer.appendChild(itemEl);
......@@ -55,7 +55,9 @@ class FakePDFPageProxy {
getTextContent(params = {}) {
if (!params.normalizeWhitespace) {
return Promise.reject(new Error('Expected `normalizeWhitespace` to be true'));
return Promise.reject(
new Error('Expected `normalizeWhitespace` to be true')
);
}
const textContent = {
......
......@@ -23,12 +23,15 @@
// them as `<fixture name>.json` in this directory
// 4. Add an entry to the fixture list below.
module.exports = [{
name: 'Minimal Document',
html: require('./minimal.html'),
annotations: require('./minimal.json'),
},{
name: 'Wikipedia - Regression Testing',
html: require('./wikipedia-regression-testing.html'),
annotations: require('./wikipedia-regression-testing.json'),
}];
module.exports = [
{
name: 'Minimal Document',
html: require('./minimal.html'),
annotations: require('./minimal.json'),
},
{
name: 'Wikipedia - Regression Testing',
html: require('./wikipedia-regression-testing.html'),
annotations: require('./wikipedia-regression-testing.json'),
},
];
This diff is collapsed.
This diff is collapsed.
......@@ -8,10 +8,10 @@ const TextPositionAnchor = types.TextPositionAnchor;
// These are primarily basic API tests for the anchoring classes. Tests for
// anchoring a variety of HTML and PDF content exist in `html-test` and
// `pdf-test`.
describe('types', function () {
describe('types', function() {
let container;
before(function () {
before(function() {
container = document.createElement('div');
container.innerHTML = [
'Four score and seven years ago our fathers brought forth on this continent,',
......@@ -21,37 +21,43 @@ describe('types', function () {
document.body.appendChild(container);
});
after(function () {
after(function() {
container.remove();
});
describe('TextQuoteAnchor', function () {
describe('#toRange', function () {
it('returns a valid DOM Range', function () {
describe('TextQuoteAnchor', function() {
describe('#toRange', function() {
it('returns a valid DOM Range', function() {
const quoteAnchor = new TextQuoteAnchor(container, 'Liberty');
const range = quoteAnchor.toRange();
assert.instanceOf(range, Range);
assert.equal(range.toString(), 'Liberty');
});
it('throws if the quote is not found', function () {
const quoteAnchor = new TextQuoteAnchor(container, 'five score and nine years ago');
assert.throws(function () {
it('throws if the quote is not found', function() {
const quoteAnchor = new TextQuoteAnchor(
container,
'five score and nine years ago'
);
assert.throws(function() {
quoteAnchor.toRange();
});
});
});
describe('#toPositionAnchor', function () {
it('returns a TextPositionAnchor', function () {
describe('#toPositionAnchor', function() {
it('returns a TextPositionAnchor', function() {
const quoteAnchor = new TextQuoteAnchor(container, 'Liberty');
const pos = quoteAnchor.toPositionAnchor();
assert.instanceOf(pos, TextPositionAnchor);
});
it('throws if the quote is not found', function () {
const quoteAnchor = new TextQuoteAnchor(container, 'some are more equal than others');
assert.throws(function () {
it('throws if the quote is not found', function() {
const quoteAnchor = new TextQuoteAnchor(
container,
'some are more equal than others'
);
assert.throws(function() {
quoteAnchor.toPositionAnchor();
});
});
......
......@@ -13,10 +13,13 @@ const ANNOTATION_COUNT_ATTR = 'data-hypothesis-annotation-count';
*/
function annotationCounts(rootEl, crossframe) {
crossframe.on(events.PUBLIC_ANNOTATION_COUNT_CHANGED, updateAnnotationCountElems);
crossframe.on(
events.PUBLIC_ANNOTATION_COUNT_CHANGED,
updateAnnotationCountElems
);
function updateAnnotationCountElems(newCount) {
const elems = rootEl.querySelectorAll('['+ANNOTATION_COUNT_ATTR+']');
const elems = rootEl.querySelectorAll('[' + ANNOTATION_COUNT_ATTR + ']');
Array.from(elems).forEach(function(elem) {
elem.textContent = newCount;
});
......
......@@ -45,7 +45,7 @@ function AnnotationSync(bridge, options) {
AnnotationSync.prototype.cache = null;
AnnotationSync.prototype.sync = function(annotations) {
annotations = (function() {
annotations = function() {
let i;
const formattedAnnotations = [];
......@@ -53,32 +53,36 @@ AnnotationSync.prototype.sync = function(annotations) {
formattedAnnotations.push(this._format(annotations[i]));
}
return formattedAnnotations;
}).call(this);
this.bridge.call('sync', annotations, (function(_this) {
return function(err, annotations) {
let i;
const parsedAnnotations = [];
annotations = annotations || [];
for (i = 0; i < annotations.length; i++) {
parsedAnnotations.push(_this._parse(annotations[i]));
}
return parsedAnnotations;
};
})(this));
}.call(this);
this.bridge.call(
'sync',
annotations,
(function(_this) {
return function(err, annotations) {
let i;
const parsedAnnotations = [];
annotations = annotations || [];
for (i = 0; i < annotations.length; i++) {
parsedAnnotations.push(_this._parse(annotations[i]));
}
return parsedAnnotations;
};
})(this)
);
return this;
};
// Handlers for messages arriving through a channel
AnnotationSync.prototype._channelListeners = {
'deleteAnnotation': function(body, cb) {
deleteAnnotation: function(body, cb) {
const annotation = this._parse(body);
delete this.cache[annotation.$tag];
this._emit('annotationDeleted', annotation);
cb(null, this._format(annotation));
},
'loadAnnotations': function(bodies, cb) {
const annotations = (function() {
loadAnnotations: function(bodies, cb) {
const annotations = function() {
let i;
const parsedAnnotations = [];
......@@ -86,7 +90,7 @@ AnnotationSync.prototype._channelListeners = {
parsedAnnotations.push(this._parse(bodies[i]));
}
return parsedAnnotations;
}).call(this);
}.call(this);
this._emit('annotationsLoaded', annotations);
return cb(null, annotations);
},
......@@ -94,15 +98,20 @@ AnnotationSync.prototype._channelListeners = {
// Handlers for events coming from this frame, to send them across the channel
AnnotationSync.prototype._eventListeners = {
'beforeAnnotationCreated': function(annotation) {
beforeAnnotationCreated: function(annotation) {
if (annotation.$tag) {
return undefined;
}
return this._mkCallRemotelyAndParseResults('beforeCreateAnnotation')(annotation);
return this._mkCallRemotelyAndParseResults('beforeCreateAnnotation')(
annotation
);
},
};
AnnotationSync.prototype._mkCallRemotelyAndParseResults = function(method, callBack) {
AnnotationSync.prototype._mkCallRemotelyAndParseResults = function(
method,
callBack
) {
return (function(_this) {
return function(annotation) {
// Wrap the callback function to first parse returned items
......
......@@ -24,7 +24,8 @@ function configFuncSettingsFrom(window_) {
}
if (typeof window_.hypothesisConfig !== 'function') {
const docs = 'https://h.readthedocs.io/projects/client/en/latest/publishers/config/#window.hypothesisConfig';
const docs =
'https://h.readthedocs.io/projects/client/en/latest/publishers/config/#window.hypothesisConfig';
console.warn('hypothesisConfig must be a function, see: ' + docs);
return {};
}
......
......@@ -13,16 +13,22 @@ function configFrom(window_) {
annotations: settings.annotations,
// URL where client assets are served from. Used when injecting the client
// into child iframes.
assetRoot: settings.hostPageSetting('assetRoot', {allowInBrowserExt: true}),
assetRoot: settings.hostPageSetting('assetRoot', {
allowInBrowserExt: true,
}),
branding: settings.hostPageSetting('branding'),
// URL of the client's boot script. Used when injecting the client into
// child iframes.
clientUrl: settings.clientUrl,
enableExperimentalNewNoteButton: settings.hostPageSetting('enableExperimentalNewNoteButton'),
enableExperimentalNewNoteButton: settings.hostPageSetting(
'enableExperimentalNewNoteButton'
),
theme: settings.hostPageSetting('theme'),
usernameUrl: settings.hostPageSetting('usernameUrl'),
onLayoutChange: settings.hostPageSetting('onLayoutChange'),
openSidebar: settings.hostPageSetting('openSidebar', {allowInBrowserExt: true}),
openSidebar: settings.hostPageSetting('openSidebar', {
allowInBrowserExt: true,
}),
query: settings.query,
requestConfigFromFrame: settings.hostPageSetting('requestConfigFromFrame'),
services: settings.hostPageSetting('services'),
......@@ -30,8 +36,12 @@ function configFrom(window_) {
sidebarAppUrl: settings.sidebarAppUrl,
// Subframe identifier given when a frame is being embedded into
// by a top level client
subFrameIdentifier: settings.hostPageSetting('subFrameIdentifier', {allowInBrowserExt: true}),
externalContainerSelector: settings.hostPageSetting('externalContainerSelector'),
subFrameIdentifier: settings.hostPageSetting('subFrameIdentifier', {
allowInBrowserExt: true,
}),
externalContainerSelector: settings.hostPageSetting(
'externalContainerSelector'
),
};
}
......
......@@ -5,7 +5,6 @@ const isBrowserExtension = require('./is-browser-extension');
const sharedSettings = require('../../shared/settings');
function settingsFrom(window_) {
const jsonConfigs = sharedSettings.jsonConfigsFrom(window_.document);
const configFuncSettings = configFuncSettingsFrom(window_);
......@@ -24,20 +23,25 @@ function settingsFrom(window_) {
*
*/
function sidebarAppUrl() {
const link = window_.document.querySelector('link[type="application/annotator+html"][rel="sidebar"]');
const link = window_.document.querySelector(
'link[type="application/annotator+html"][rel="sidebar"]'
);
if (!link) {
throw new Error('No application/annotator+html (rel="sidebar") link in the document');
throw new Error(
'No application/annotator+html (rel="sidebar") link in the document'
);
}
if (!link.href) {
throw new Error('application/annotator+html (rel="sidebar") link has no href');
throw new Error(
'application/annotator+html (rel="sidebar") link has no href'
);
}
return link.href;
}
/**
* Return the href URL of the first annotator client link in the given document.
*
......@@ -54,14 +58,20 @@ function settingsFrom(window_) {
*
*/
function clientUrl() {
const link = window_.document.querySelector('link[type="application/annotator+javascript"][rel="hypothesis-client"]');
const link = window_.document.querySelector(
'link[type="application/annotator+javascript"][rel="hypothesis-client"]'
);
if (!link) {
throw new Error('No application/annotator+javascript (rel="hypothesis-client") link in the document');
throw new Error(
'No application/annotator+javascript (rel="hypothesis-client") link in the document'
);
}
if (!link.href) {
throw new Error('application/annotator+javascript (rel="hypothesis-client") link has no href');
throw new Error(
'application/annotator+javascript (rel="hypothesis-client") link has no href'
);
}
return link.href;
......@@ -76,12 +86,13 @@ function settingsFrom(window_) {
* @return {string|null} - The extracted ID, or null.
*/
function annotations() {
/** Return the annotations from the URL, or null. */
function annotationsFromURL() {
// Annotation IDs are url-safe-base64 identifiers
// See https://tools.ietf.org/html/rfc4648#page-7
const annotFragmentMatch = window_.location.href.match(/#annotations:([A-Za-z0-9_-]+)$/);
const annotFragmentMatch = window_.location.href.match(
/#annotations:([A-Za-z0-9_-]+)$/
);
if (annotFragmentMatch) {
return annotFragmentMatch[1];
}
......@@ -95,7 +106,7 @@ function settingsFrom(window_) {
let showHighlights_ = hostPageSetting('showHighlights');
if (showHighlights_ === null) {
showHighlights_ = 'always'; // The default value is 'always'.
showHighlights_ = 'always'; // The default value is 'always'.
}
// Convert legacy keys/values to corresponding current configuration.
......@@ -120,10 +131,11 @@ function settingsFrom(window_) {
* @return {string|null} - The config.query setting, or null.
*/
function query() {
/** Return the query from the URL, or null. */
function queryFromURL() {
const queryFragmentMatch = window_.location.href.match(/#annotations:(query|q):(.+)$/i);
const queryFragmentMatch = window_.location.href.match(
/#annotations:(query|q):(.+)$/i
);
if (queryFragmentMatch) {
try {
return decodeURIComponent(queryFragmentMatch[2]);
......@@ -161,11 +173,21 @@ function settingsFrom(window_) {
}
return {
get annotations() { return annotations(); },
get clientUrl() { return clientUrl(); },
get showHighlights() { return showHighlights(); },
get sidebarAppUrl() { return sidebarAppUrl(); },
get query() { return query(); },
get annotations() {
return annotations();
},
get clientUrl() {
return clientUrl();
},
get showHighlights() {
return showHighlights();
},
get sidebarAppUrl() {
return sidebarAppUrl();
},
get query() {
return query();
},
hostPageSetting: hostPageSetting,
};
}
......
......@@ -23,7 +23,7 @@ describe('annotator.config.configFuncSettingsFrom', function() {
});
function fakeWindow() {
return {hypothesisConfig: 42};
return { hypothesisConfig: 42 };
}
it('returns {}', function() {
......@@ -34,14 +34,16 @@ describe('annotator.config.configFuncSettingsFrom', function() {
configFuncSettingsFrom(fakeWindow());
assert.calledOnce(console.warn);
assert.isTrue(console.warn.firstCall.args[0].startsWith(
'hypothesisConfig must be a function'
));
assert.isTrue(
console.warn.firstCall.args[0].startsWith(
'hypothesisConfig must be a function'
)
);
});
});
context('when window.hypothesisConfig() is a function', function() {
it('returns whatever window.hypothesisConfig() returns', function () {
it('returns whatever window.hypothesisConfig() returns', function() {
// It just blindly returns whatever hypothesisConfig() returns
// (even if it's not an object).
const fakeWindow = { hypothesisConfig: sinon.stub().returns(42) };
......
......@@ -5,12 +5,14 @@ const util = require('../../../shared/test/util');
const fakeSettingsFrom = sinon.stub();
const configFrom = proxyquire('../index', util.noCallThru({
'./settings': fakeSettingsFrom,
}));
const configFrom = proxyquire(
'../index',
util.noCallThru({
'./settings': fakeSettingsFrom,
})
);
describe('annotator.config.index', function() {
beforeEach('reset fakeSettingsFrom', function() {
fakeSettingsFrom.reset();
fakeSettingsFrom.returns({
......@@ -25,12 +27,9 @@ describe('annotator.config.index', function() {
assert.calledWithExactly(fakeSettingsFrom, 'WINDOW');
});
[
'sidebarAppUrl',
'query',
'annotations',
'showHighlights',
].forEach(function(settingName) {
['sidebarAppUrl', 'query', 'annotations', 'showHighlights'].forEach(function(
settingName
) {
it('returns the ' + settingName + ' setting', function() {
fakeSettingsFrom()[settingName] = 'SETTING_VALUE';
......@@ -42,47 +41,51 @@ describe('annotator.config.index', function() {
context("when there's no application/annotator+html <link>", function() {
beforeEach('remove the application/annotator+html <link>', function() {
Object.defineProperty(
fakeSettingsFrom(),
'sidebarAppUrl',
{
get: sinon.stub().throws(new Error("there's no link")),
}
);
Object.defineProperty(fakeSettingsFrom(), 'sidebarAppUrl', {
get: sinon.stub().throws(new Error("there's no link")),
});
});
it('throws an error', function() {
assert.throws(
function() { configFrom('WINDOW'); },
"there's no link"
);
assert.throws(function() {
configFrom('WINDOW');
}, "there's no link");
});
});
[
'assetRoot',
'subFrameIdentifier',
'openSidebar',
].forEach(function(settingName) {
it('reads ' + settingName + ' from the host page, even when in a browser extension', function() {
configFrom('WINDOW');
assert.calledWithExactly(
fakeSettingsFrom().hostPageSetting,
settingName, {allowInBrowserExt: true}
);
});
['assetRoot', 'subFrameIdentifier', 'openSidebar'].forEach(function(
settingName
) {
it(
'reads ' +
settingName +
' from the host page, even when in a browser extension',
function() {
configFrom('WINDOW');
assert.calledWithExactly(
fakeSettingsFrom().hostPageSetting,
settingName,
{ allowInBrowserExt: true }
);
}
);
});
[
'branding',
'services',
].forEach(function(settingName) {
it('reads ' + settingName + ' from the host page only when in an embedded client', function() {
configFrom('WINDOW');
assert.calledWithExactly(fakeSettingsFrom().hostPageSetting, settingName);
});
['branding', 'services'].forEach(function(settingName) {
it(
'reads ' +
settingName +
' from the host page only when in an embedded client',
function() {
configFrom('WINDOW');
assert.calledWithExactly(
fakeSettingsFrom().hostPageSetting,
settingName
);
}
);
});
[
......@@ -94,11 +97,11 @@ describe('annotator.config.index', function() {
].forEach(function(settingName) {
it('returns the ' + settingName + ' value from the host page', function() {
const settings = {
'assetRoot': 'chrome-extension://1234/client/',
'branding': 'BRANDING_SETTING',
'openSidebar': 'OPEN_SIDEBAR_SETTING',
'requestConfigFromFrame': 'https://embedder.com',
'services': 'SERVICES_SETTING',
assetRoot: 'chrome-extension://1234/client/',
branding: 'BRANDING_SETTING',
openSidebar: 'OPEN_SIDEBAR_SETTING',
requestConfigFromFrame: 'https://embedder.com',
services: 'SERVICES_SETTING',
};
fakeSettingsFrom().hostPageSetting = function(settingName) {
return settings[settingName];
......
This diff is collapsed.
......@@ -4,13 +4,11 @@ const events = require('../shared/bridge-events');
let _features = {};
const _set = (features) => {
const _set = features => {
_features = features || {};
};
module.exports = {
init: function(crossframe) {
crossframe.on(events.FEATURE_FLAGS_UPDATED, _set);
},
......@@ -26,5 +24,4 @@ module.exports = {
}
return _features[flag];
},
};
......@@ -11,17 +11,18 @@ let difference = (arrayA, arrayB) => {
const DEBOUNCE_WAIT = 40;
class FrameObserver {
constructor (target) {
constructor(target) {
this._target = target;
this._handledFrames = [];
this._mutationObserver = new MutationObserver(debounce(() => {
this._discoverFrames();
}, DEBOUNCE_WAIT));
this._mutationObserver = new MutationObserver(
debounce(() => {
this._discoverFrames();
}, DEBOUNCE_WAIT)
);
}
observe (onFrameAddedCallback, onFrameRemovedCallback) {
observe(onFrameAddedCallback, onFrameRemovedCallback) {
this._onFrameAdded = onFrameAddedCallback;
this._onFrameRemoved = onFrameRemovedCallback;
......@@ -32,11 +33,11 @@ class FrameObserver {
});
}
disconnect () {
disconnect() {
this._mutationObserver.disconnect();
}
_addFrame (frame) {
_addFrame(frame) {
if (FrameUtil.isAccessible(frame)) {
FrameUtil.isDocumentReady(frame, () => {
frame.contentWindow.addEventListener('unload', () => {
......@@ -50,14 +51,14 @@ class FrameObserver {
}
}
_removeFrame (frame) {
_removeFrame(frame) {
this._onFrameRemoved(frame);
// Remove the frame from our list
this._handledFrames = this._handledFrames.filter(x => x !== frame);
}
_discoverFrames () {
_discoverFrames() {
let frames = FrameUtil.findFrames(this._target);
for (let frame of frames) {
......@@ -73,4 +74,4 @@ class FrameObserver {
}
FrameObserver.DEBOUNCE_WAIT = DEBOUNCE_WAIT;
module.exports = FrameObserver;
\ No newline at end of file
module.exports = FrameObserver;
......@@ -10,16 +10,18 @@ const features = require('../features');
const highlighterFacade = {};
let overlayFlagEnabled;
Object.keys(domWrapHighlighter).forEach((methodName)=>{
highlighterFacade[methodName] = (...args)=>{
Object.keys(domWrapHighlighter).forEach(methodName => {
highlighterFacade[methodName] = (...args) => {
// lazy check the value but we will
// use that first value as the rule throughout
// the in memory session
if(overlayFlagEnabled === undefined){
if (overlayFlagEnabled === undefined) {
overlayFlagEnabled = features.flagEnabled('overlay_highlighter');
}
const method = overlayFlagEnabled ? overlayHighlighter[methodName] : domWrapHighlighter[methodName];
const method = overlayFlagEnabled
? overlayHighlighter[methodName]
: domWrapHighlighter[methodName];
return method.apply(null, args);
};
});
......
......@@ -3,7 +3,6 @@
const configFrom = require('./config/index');
require('../shared/polyfills');
// Polyfills
// document.evaluate() implementation,
......@@ -37,13 +36,13 @@ const pluginClasses = {
CrossFrame: require('./plugin/cross-frame'),
};
const appLinkEl = document.querySelector('link[type="application/annotator+html"][rel="sidebar"]');
const appLinkEl = document.querySelector(
'link[type="application/annotator+html"][rel="sidebar"]'
);
const config = configFrom(window);
$.noConflict(true)(function() {
let Klass = window.PDFViewerApplication ?
PdfSidebar :
Sidebar;
let Klass = window.PDFViewerApplication ? PdfSidebar : Sidebar;
if (config.hasOwnProperty('constructor')) {
Klass = config.constructor;
......@@ -66,7 +65,7 @@ $.noConflict(true)(function() {
config.pluginClasses = pluginClasses;
const annotator = new Klass(document.body, config);
appLinkEl.addEventListener('destroy', function () {
appLinkEl.addEventListener('destroy', function() {
appLinkEl.parentElement.removeChild(appLinkEl);
annotator.destroy();
});
......
'use strict';
/*
** Adapted from:
** https://github.com/openannotation/annotator/blob/v1.2.x/src/plugin/document.coffee
**
** Annotator v1.2.10
** https://github.com/openannotation/annotator
**
** Copyright 2015, the Annotator project contributors.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/openannotation/annotator/blob/master/LICENSE
*/
** Adapted from:
** https://github.com/openannotation/annotator/blob/v1.2.x/src/plugin/document.coffee
**
** Annotator v1.2.10
** https://github.com/openannotation/annotator
**
** Copyright 2015, the Annotator project contributors.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/openannotation/annotator/blob/master/LICENSE
*/
const baseURI = require('document-base-uri');
......@@ -26,7 +26,7 @@ class DocumentMeta extends Plugin {
super(element, options);
this.events = {
'beforeAnnotationCreated': 'beforeAnnotationCreated',
beforeAnnotationCreated: 'beforeAnnotationCreated',
};
}
......@@ -62,7 +62,9 @@ class DocumentMeta extends Plugin {
uris() {
const uniqueUrls = {};
for (let link of this.metadata.link) {
if (link.href) { uniqueUrls[link.href] = true; }
if (link.href) {
uniqueUrls[link.href] = true;
}
}
return Object.keys(uniqueUrls);
}
......@@ -162,12 +164,14 @@ class DocumentMeta extends Plugin {
_getLinks() {
// We know our current location is a link for the document.
this.metadata.link = [{href: this._getDocumentHref()}];
this.metadata.link = [{ href: this._getDocumentHref() }];
// Extract links from certain `<link>` tags.
const linkElements = Array.from(this.document.querySelectorAll('link'));
for (let link of linkElements) {
if (!['alternate', 'canonical', 'bookmark', 'shortlink'].includes(link.rel)) {
if (
!['alternate', 'canonical', 'bookmark', 'shortlink'].includes(link.rel)
) {
continue;
}
......@@ -184,7 +188,7 @@ class DocumentMeta extends Plugin {
try {
const href = this._absoluteUrl(link.href);
this.metadata.link.push({href, rel: link.rel, type: link.type});
this.metadata.link.push({ href, rel: link.rel, type: link.type });
} catch (e) {
// Ignore URIs which cannot be parsed.
}
......@@ -214,7 +218,7 @@ class DocumentMeta extends Plugin {
if (doi.slice(0, 4) !== 'doi:') {
doi = `doi:${doi}`;
}
this.metadata.link.push({href: doi});
this.metadata.link.push({ href: doi });
}
}
}
......@@ -225,7 +229,7 @@ class DocumentMeta extends Plugin {
if (name === 'identifier') {
for (let id of values) {
if (id.slice(0, 4) === 'doi:') {
this.metadata.link.push({href: id});
this.metadata.link.push({ href: id });
}
}
}
......@@ -239,10 +243,12 @@ class DocumentMeta extends Plugin {
dcRelationValues[dcRelationValues.length - 1];
const dcUrnIdentifierComponent =
dcIdentifierValues[dcIdentifierValues.length - 1];
const dcUrn = 'urn:x-dc:' +
encodeURIComponent(dcUrnRelationComponent) + '/' +
const dcUrn =
'urn:x-dc:' +
encodeURIComponent(dcUrnRelationComponent) +
'/' +
encodeURIComponent(dcUrnIdentifierComponent);
this.metadata.link.push({href: dcUrn});
this.metadata.link.push({ href: dcUrn });
// set this as the documentFingerprint as a hint to include this in search queries
this.metadata.documentFingerprint = dcUrn;
}
......@@ -282,7 +288,10 @@ class DocumentMeta extends Plugin {
}
// Otherwise, try using the location specified by the <base> element.
if (this.baseURI && allowedSchemes.includes(new URL(this.baseURI).protocol)) {
if (
this.baseURI &&
allowedSchemes.includes(new URL(this.baseURI).protocol)
) {
return this.baseURI;
}
......
......@@ -86,19 +86,21 @@ class PDFMetadata {
return this._loaded.then(app => {
let title = document.title;
if (app.metadata && app.metadata.has('dc:title') && app.metadata.get('dc:title') !== 'Untitled') {
if (
app.metadata &&
app.metadata.has('dc:title') &&
app.metadata.get('dc:title') !== 'Untitled'
) {
title = app.metadata.get('dc:title');
} else if (app.documentInfo && app.documentInfo.Title) {
title = app.documentInfo.Title;
}
const link = [
{href: fingerprintToURN(app.pdfDocument.fingerprint)},
];
const link = [{ href: fingerprintToURN(app.pdfDocument.fingerprint) }];
const url = getPDFURL(app);
if (url) {
link.push({href: url});
link.push({ href: url });
}
return {
......
'use strict';
/*
** Adapted from:
** https://github.com/openannotation/annotator/blob/v1.2.x/test/spec/plugin/document_spec.coffee
**
** Annotator v1.2.10
** https://github.com/openannotation/annotator
**
** Copyright 2015, the Annotator project contributors.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/openannotation/annotator/blob/master/LICENSE
*/
** Adapted from:
** https://github.com/openannotation/annotator/blob/v1.2.x/test/spec/plugin/document_spec.coffee
**
** Annotator v1.2.10
** https://github.com/openannotation/annotator
**
** Copyright 2015, the Annotator project contributors.
** Dual licensed under the MIT and GPLv3 licenses.
** https://github.com/openannotation/annotator/blob/master/LICENSE
*/
const $ = require('jquery');
......@@ -126,7 +126,10 @@ describe('DocumentMeta', function() {
it('should have dublincore metadata', function() {
assert.ok(metadata.dc);
assert.deepEqual(metadata.dc.identifier, ['doi:10.1175/JCLI-D-11-00015.1', 'foobar-abcxyz']);
assert.deepEqual(metadata.dc.identifier, [
'doi:10.1175/JCLI-D-11-00015.1',
'foobar-abcxyz',
]);
assert.deepEqual(metadata.dc['relation.ispartof'], ['isbn:123456789']);
assert.deepEqual(metadata.dc.type, ['Article']);
});
......@@ -138,7 +141,9 @@ describe('DocumentMeta', function() {
it('should have eprints metadata', function() {
assert.ok(metadata.eprints);
assert.deepEqual(metadata.eprints.title, ['Computer Lib / Dream Machines']);
assert.deepEqual(metadata.eprints.title, [
'Computer Lib / Dream Machines',
]);
});
it('should have prism metadata', function() {
......@@ -162,11 +167,7 @@ describe('DocumentMeta', function() {
});
it('should have a favicon', () =>
assert.equal(
metadata.favicon,
'http://example.com/images/icon.ico'
)
);
assert.equal(metadata.favicon, 'http://example.com/images/icon.ico'));
it('should have a documentFingerprint as the dc resource identifiers URN href', () => {
assert.equal(metadata.documentFingerprint, metadata.link[9].href);
......@@ -210,9 +211,7 @@ describe('DocumentMeta', function() {
});
});
describe('#_absoluteUrl', function() {
it('should add the protocol when the url starts with two slashes', function() {
const result = testDocument._absoluteUrl('//example.com/');
const expected = `${document.location.protocol}//example.com/`;
......@@ -226,28 +225,24 @@ describe('DocumentMeta', function() {
it('should make a relative path into an absolute url', function() {
const result = testDocument._absoluteUrl('path');
const expected = (
document.location.protocol + '//' +
document.location.host +
document.location.pathname.replace(/[^/]+$/, '') +
'path'
);
const expected =
document.location.protocol +
'//' +
document.location.host +
document.location.pathname.replace(/[^/]+$/, '') +
'path';
assert.equal(result, expected);
});
it('should make an absolute path into an absolute url', function() {
const result = testDocument._absoluteUrl('/path');
const expected = (
document.location.protocol + '//' +
document.location.host +
'/path'
);
const expected =
document.location.protocol + '//' + document.location.host + '/path';
assert.equal(result, expected);
});
});
describe('#uri', function() {
beforeEach(function() {
// Remove any existing canonical links which would otherwise override the
// document's own location.
......@@ -270,7 +265,7 @@ describe('DocumentMeta', function() {
// document.
const fakeDocument = {
createElement: htmlDoc.createElement.bind(htmlDoc), // eslint-disable-line no-restricted-properties
querySelectorAll: htmlDoc.querySelectorAll.bind(htmlDoc), // eslint-disable-line no-restricted-properties
querySelectorAll: htmlDoc.querySelectorAll.bind(htmlDoc), // eslint-disable-line no-restricted-properties
location: {
href,
},
......@@ -325,7 +320,11 @@ describe('DocumentMeta', function() {
canonicalLink.href = 'https://publisher.org/canonical';
htmlDoc.head.appendChild(canonicalLink);
const doc = createDoc('https://publisher.org/not-canonical', null, htmlDoc);
const doc = createDoc(
'https://publisher.org/not-canonical',
null,
htmlDoc
);
assert.equal(doc.uri(), canonicalLink.href);
});
......
......@@ -53,7 +53,13 @@ class FakePDFViewerApplication {
/**
* Simulate completion of PDF document loading.
*/
finishLoading({ url, fingerprint, metadata, title, eventName = 'documentload' }) {
finishLoading({
url,
fingerprint,
metadata,
title,
eventName = 'documentload',
}) {
const event = document.createEvent('Event');
event.initEvent(eventName, false, false);
window.dispatchEvent(event);
......@@ -74,15 +80,15 @@ class FakePDFViewerApplication {
}
}
describe('annotator/plugin/pdf-metadata', function () {
describe('annotator/plugin/pdf-metadata', function() {
[
// Event dispatched in older PDF.js versions (pre-7bc4bfcc).
'documentload',
// Event dispatched in newer PDF.js versions (post-7bc4bfcc).
'documentloaded',
].forEach(eventName => {
it('waits for the PDF to load before returning metadata', function () {
const fakeApp = new FakePDFViewerApplication;
it('waits for the PDF to load before returning metadata', function() {
const fakeApp = new FakePDFViewerApplication();
const pdfMetadata = new PDFMetadata(fakeApp);
fakeApp.finishLoading({
......@@ -91,27 +97,27 @@ describe('annotator/plugin/pdf-metadata', function () {
fingerprint: 'fakeFingerprint',
});
return pdfMetadata.getUri().then(function (uri) {
return pdfMetadata.getUri().then(function(uri) {
assert.equal(uri, 'http://fake.com/');
});
});
});
it('does not wait for the PDF to load if it has already loaded', function () {
const fakePDFViewerApplication = new FakePDFViewerApplication;
it('does not wait for the PDF to load if it has already loaded', function() {
const fakePDFViewerApplication = new FakePDFViewerApplication();
fakePDFViewerApplication.finishLoading({
url: 'http://fake.com',
fingerprint: 'fakeFingerprint',
});
const pdfMetadata = new PDFMetadata(fakePDFViewerApplication);
return pdfMetadata.getUri().then(function (uri) {
return pdfMetadata.getUri().then(function(uri) {
assert.equal(uri, 'http://fake.com/');
});
});
describe('metadata sources', function () {
describe('metadata sources', function() {
let pdfMetadata;
const fakePDFViewerApplication = new FakePDFViewerApplication;
const fakePDFViewerApplication = new FakePDFViewerApplication();
fakePDFViewerApplication.finishLoading({
fingerprint: 'fakeFingerprint',
title: 'fakeTitle',
......@@ -121,32 +127,32 @@ describe('annotator/plugin/pdf-metadata', function () {
url: 'http://fake.com/',
});
beforeEach(function () {
beforeEach(function() {
pdfMetadata = new PDFMetadata(fakePDFViewerApplication);
});
describe('#getUri', function () {
describe('#getUri', function() {
it('returns the non-file URI', function() {
return pdfMetadata.getUri().then(function (uri) {
return pdfMetadata.getUri().then(function(uri) {
assert.equal(uri, 'http://fake.com/');
});
});
it('returns the fingerprint as a URN when the PDF URL is a local file', function () {
const fakePDFViewerApplication = new FakePDFViewerApplication;
it('returns the fingerprint as a URN when the PDF URL is a local file', function() {
const fakePDFViewerApplication = new FakePDFViewerApplication();
fakePDFViewerApplication.finishLoading({
url: 'file:///test.pdf',
fingerprint: 'fakeFingerprint',
});
const pdfMetadata = new PDFMetadata(fakePDFViewerApplication);
return pdfMetadata.getUri().then(function (uri) {
return pdfMetadata.getUri().then(function(uri) {
assert.equal(uri, 'urn:x-pdf:fakeFingerprint');
});
});
it('resolves relative URLs', () => {
const fakePDFViewerApplication = new FakePDFViewerApplication;
const fakePDFViewerApplication = new FakePDFViewerApplication();
fakePDFViewerApplication.finishLoading({
url: 'index.php?action=download&file_id=wibble',
fingerprint: 'fakeFingerprint',
......@@ -154,58 +160,78 @@ describe('annotator/plugin/pdf-metadata', function () {
const pdfMetadata = new PDFMetadata(fakePDFViewerApplication);
return pdfMetadata.getUri().then(uri => {
const expected = new URL(fakePDFViewerApplication.url,
document.location.href).toString();
const expected = new URL(
fakePDFViewerApplication.url,
document.location.href
).toString();
assert.equal(uri, expected);
});
});
});
describe('#getMetadata', function () {
it('gets the title from the dc:title field', function () {
describe('#getMetadata', function() {
it('gets the title from the dc:title field', function() {
const expectedMetadata = {
title: 'dcFakeTitle',
link: [{href: 'urn:x-pdf:' + fakePDFViewerApplication.pdfDocument.fingerprint},
{href: fakePDFViewerApplication.url}],
link: [
{
href:
'urn:x-pdf:' + fakePDFViewerApplication.pdfDocument.fingerprint,
},
{ href: fakePDFViewerApplication.url },
],
documentFingerprint: fakePDFViewerApplication.pdfDocument.fingerprint,
};
return pdfMetadata.getMetadata().then(function (actualMetadata) {
return pdfMetadata.getMetadata().then(function(actualMetadata) {
assert.deepEqual(actualMetadata, expectedMetadata);
});
});
it('gets the title from the documentInfo.Title field', function () {
it('gets the title from the documentInfo.Title field', function() {
const expectedMetadata = {
title: fakePDFViewerApplication.documentInfo.Title,
link: [{href: 'urn:x-pdf:' + fakePDFViewerApplication.pdfDocument.fingerprint},
{href: fakePDFViewerApplication.url}],
link: [
{
href:
'urn:x-pdf:' + fakePDFViewerApplication.pdfDocument.fingerprint,
},
{ href: fakePDFViewerApplication.url },
],
documentFingerprint: fakePDFViewerApplication.pdfDocument.fingerprint,
};
fakePDFViewerApplication.metadata.has = sinon.stub().returns(false);
return pdfMetadata.getMetadata().then(function (actualMetadata) {
return pdfMetadata.getMetadata().then(function(actualMetadata) {
assert.deepEqual(actualMetadata, expectedMetadata);
});
});
it('does not save file:// URLs in document metadata', function () {
it('does not save file:// URLs in document metadata', function() {
let pdfMetadata;
const fakePDFViewerApplication = new FakePDFViewerApplication;
const fakePDFViewerApplication = new FakePDFViewerApplication();
fakePDFViewerApplication.finishLoading({
fingerprint: 'fakeFingerprint',
url: 'file://fake.pdf',
});
const expectedMetadata = {
link: [{href: 'urn:x-pdf:' + fakePDFViewerApplication.pdfDocument.fingerprint}],
link: [
{
href:
'urn:x-pdf:' + fakePDFViewerApplication.pdfDocument.fingerprint,
},
],
};
pdfMetadata = new PDFMetadata(fakePDFViewerApplication);
return pdfMetadata.getMetadata().then(function (actualMetadata) {
return pdfMetadata.getMetadata().then(function(actualMetadata) {
assert.equal(actualMetadata.link.length, 1);
assert.equal(actualMetadata.link[0].href, expectedMetadata.link[0].href);
assert.equal(
actualMetadata.link[0].href,
expectedMetadata.link[0].href
);
});
});
});
......
......@@ -49,11 +49,16 @@ function forEachNodeInRange(range, callback) {
// The `whatToShow`, `filter` and `expandEntityReferences` arguments are
// mandatory in IE although optional according to the spec.
const nodeIter = root.ownerDocument.createNodeIterator(root,
NodeFilter.SHOW_ALL, null /* filter */, false /* expandEntityReferences */);
const nodeIter = root.ownerDocument.createNodeIterator(
root,
NodeFilter.SHOW_ALL,
null /* filter */,
false /* expandEntityReferences */
);
let currentNode;
while (currentNode = nodeIter.nextNode()) { // eslint-disable-line no-cond-assign
while ((currentNode = nodeIter.nextNode())) {
// eslint-disable-line no-cond-assign
if (isNodeInRange(range, currentNode)) {
callback(currentNode);
}
......@@ -69,15 +74,17 @@ function forEachNodeInRange(range, callback) {
function getTextBoundingBoxes(range) {
const whitespaceOnly = /^\s*$/;
const textNodes = [];
forEachNodeInRange(range, function (node) {
if (node.nodeType === Node.TEXT_NODE &&
!node.textContent.match(whitespaceOnly)) {
forEachNodeInRange(range, function(node) {
if (
node.nodeType === Node.TEXT_NODE &&
!node.textContent.match(whitespaceOnly)
) {
textNodes.push(node);
}
});
let rects = [];
textNodes.forEach(function (node) {
textNodes.forEach(function(node) {
const nodeRange = node.ownerDocument.createRange();
nodeRange.selectNodeContents(node);
if (node === range.startContainer) {
......
......@@ -24,13 +24,12 @@ function selectedRange(document) {
* @return Observable<DOMRange|null>
*/
function selections(document) {
// Get a stream of selection changes that occur whilst the user is not
// making a selection with the mouse.
let isMouseDown;
const selectionEvents = observable.listen(document,
['mousedown', 'mouseup', 'selectionchange'])
.filter(function (event) {
const selectionEvents = observable
.listen(document, ['mousedown', 'mouseup', 'selectionchange'])
.filter(function(event) {
if (event.type === 'mousedown' || event.type === 'mouseup') {
isMouseDown = event.type === 'mousedown';
return false;
......@@ -53,7 +52,7 @@ function selections(document) {
observable.delay(0, observable.Observable.of({})),
]);
return events.map(function () {
return events.map(function() {
return selectedRange(document);
});
}
......
......@@ -11,8 +11,9 @@ const SIDEBAR_TRIGGER_BTN_ATTR = 'data-hypothesis-trigger';
*/
function trigger(rootEl, showFn) {
const triggerElems = rootEl.querySelectorAll('['+SIDEBAR_TRIGGER_BTN_ATTR+']');
const triggerElems = rootEl.querySelectorAll(
'[' + SIDEBAR_TRIGGER_BTN_ATTR + ']'
);
Array.from(triggerElems).forEach(function(triggerElem) {
triggerElem.addEventListener('click', handleCommand);
......
......@@ -4,7 +4,7 @@ const adder = require('../adder');
const unroll = require('../../shared/test/util').unroll;
function rect(left, top, width, height) {
return {left: left, top: top, width: width, height: height};
return { left: left, top: top, width: width, height: height };
}
/**
......@@ -25,13 +25,12 @@ function revertOffsetElement(el) {
el.style.top = '0';
}
describe('annotator.adder', function () {
describe('annotator.adder', function() {
let adderCtrl;
let adderCallbacks;
let adderEl;
beforeEach(function () {
beforeEach(function() {
adderCallbacks = {
onAnnotate: sinon.stub(),
onHighlight: sinon.stub(),
......@@ -42,58 +41,68 @@ describe('annotator.adder', function () {
adderCtrl = new adder.Adder(adderEl, adderCallbacks);
});
afterEach(function () {
afterEach(function() {
adderCtrl.hide();
adderEl.remove();
});
function windowSize() {
const window = adderCtrl.element.ownerDocument.defaultView;
return {width: window.innerWidth, height: window.innerHeight};
return { width: window.innerWidth, height: window.innerHeight };
}
function adderSize() {
const rect = adderCtrl.element.getBoundingClientRect();
return {width: rect.width, height: rect.height};
return { width: rect.width, height: rect.height };
}
context('when Shadow DOM is supported', function () {
unroll('creates the adder DOM in a shadow root (using #attachFn)', function (testCase) {
const adderEl = document.createElement('div');
let shadowEl;
// Disable use of native Shadow DOM for this element, if supported.
adderEl.createShadowRoot = null;
adderEl.attachShadow = null;
adderEl[testCase.attachFn] = sinon.spy(function () {
shadowEl = document.createElement('shadow-root');
adderEl.appendChild(shadowEl);
return shadowEl;
});
document.body.appendChild(adderEl);
new adder.Adder(adderEl, adderCallbacks);
assert.called(adderEl[testCase.attachFn]);
assert.equal(shadowEl.childNodes[0].tagName.toLowerCase(), 'hypothesis-adder-toolbar');
adderEl.remove();
},[{
attachFn: 'createShadowRoot', // Shadow DOM v0 API
},{
attachFn: 'attachShadow', // Shadow DOM v1 API
}]);
context('when Shadow DOM is supported', function() {
unroll(
'creates the adder DOM in a shadow root (using #attachFn)',
function(testCase) {
const adderEl = document.createElement('div');
let shadowEl;
// Disable use of native Shadow DOM for this element, if supported.
adderEl.createShadowRoot = null;
adderEl.attachShadow = null;
adderEl[testCase.attachFn] = sinon.spy(function() {
shadowEl = document.createElement('shadow-root');
adderEl.appendChild(shadowEl);
return shadowEl;
});
document.body.appendChild(adderEl);
new adder.Adder(adderEl, adderCallbacks);
assert.called(adderEl[testCase.attachFn]);
assert.equal(
shadowEl.childNodes[0].tagName.toLowerCase(),
'hypothesis-adder-toolbar'
);
adderEl.remove();
},
[
{
attachFn: 'createShadowRoot', // Shadow DOM v0 API
},
{
attachFn: 'attachShadow', // Shadow DOM v1 API
},
]
);
});
describe('button handling', function () {
it('calls onHighlight callback when Highlight button is clicked', function () {
describe('button handling', function() {
it('calls onHighlight callback when Highlight button is clicked', function() {
const highlightBtn = adderCtrl.element.querySelector('.js-highlight-btn');
highlightBtn.dispatchEvent(new Event('click'));
assert.called(adderCallbacks.onHighlight);
});
it('calls onAnnotate callback when Annotate button is clicked', function () {
it('calls onAnnotate callback when Annotate button is clicked', function() {
const annotateBtn = adderCtrl.element.querySelector('.js-annotate-btn');
annotateBtn.dispatchEvent(new Event('click'));
assert.called(adderCallbacks.onAnnotate);
......@@ -107,39 +116,45 @@ describe('annotator.adder', function () {
});
});
describe('#target', function () {
it('positions the adder below the selection if the selection is forwards', function () {
const target = adderCtrl.target(rect(100,200,100,20), false);
describe('#target', function() {
it('positions the adder below the selection if the selection is forwards', function() {
const target = adderCtrl.target(rect(100, 200, 100, 20), false);
assert.isAbove(target.top, 220);
assert.equal(target.arrowDirection, adder.ARROW_POINTING_UP);
});
it('positions the adder above the selection if the selection is backwards', function () {
const target = adderCtrl.target(rect(100,200,100,20), true);
it('positions the adder above the selection if the selection is backwards', function() {
const target = adderCtrl.target(rect(100, 200, 100, 20), true);
assert.isBelow(target.top, 200);
assert.equal(target.arrowDirection, adder.ARROW_POINTING_DOWN);
});
it('does not position the adder above the top of the viewport', function () {
const target = adderCtrl.target(rect(100,-100,100,20), false);
it('does not position the adder above the top of the viewport', function() {
const target = adderCtrl.target(rect(100, -100, 100, 20), false);
assert.isAtLeast(target.top, 0);
assert.equal(target.arrowDirection, adder.ARROW_POINTING_UP);
});
it('does not position the adder below the bottom of the viewport', function () {
it('does not position the adder below the bottom of the viewport', function() {
const viewSize = windowSize();
const target = adderCtrl.target(rect(0,viewSize.height + 100,10,20), false);
const target = adderCtrl.target(
rect(0, viewSize.height + 100, 10, 20),
false
);
assert.isAtMost(target.top, viewSize.height - adderSize().height);
});
it('does not position the adder beyond the right edge of the viewport', function () {
it('does not position the adder beyond the right edge of the viewport', function() {
const viewSize = windowSize();
const target = adderCtrl.target(rect(viewSize.width + 100,100,10,20), false);
const target = adderCtrl.target(
rect(viewSize.width + 100, 100, 10, 20),
false
);
assert.isAtMost(target.left, viewSize.width);
});
it('does not positon the adder beyond the left edge of the viewport', function () {
const target = adderCtrl.target(rect(-100,100,10,10), false);
it('does not positon the adder beyond the left edge of the viewport', function() {
const target = adderCtrl.target(rect(-100, 100, 10, 10), false);
assert.isAtLeast(target.left, 0);
});
});
......
......@@ -2,14 +2,14 @@
const annotationCounts = require('../annotation-counts');
describe('annotationCounts', function () {
describe('annotationCounts', function() {
let countEl1;
let countEl2;
let CrossFrame;
let fakeCrossFrame;
let sandbox;
beforeEach(function () {
beforeEach(function() {
CrossFrame = null;
fakeCrossFrame = {};
sandbox = sinon.sandbox.create();
......@@ -28,20 +28,21 @@ describe('annotationCounts', function () {
CrossFrame.returns(fakeCrossFrame);
});
afterEach(function () {
afterEach(function() {
sandbox.restore();
countEl1.remove();
countEl2.remove();
});
describe('listen for "publicAnnotationCountChanged" event', function () {
const emitEvent = function () {
describe('listen for "publicAnnotationCountChanged" event', function() {
const emitEvent = function() {
let crossFrameArgs;
let evt;
let fn;
const event = arguments[0];
const args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];
const args =
2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];
crossFrameArgs = fakeCrossFrame.on.args;
for (let i = 0, len = crossFrameArgs.length; i < len; i++) {
......@@ -54,7 +55,7 @@ describe('annotationCounts', function () {
}
};
it('displays the updated annotation count on the appropriate elements', function () {
it('displays the updated annotation count on the appropriate elements', function() {
const newCount = 10;
annotationCounts(document.body, fakeCrossFrame);
......
......@@ -48,43 +48,45 @@ describe('AnnotationSync', function() {
describe('#constructor', function() {
context('when "deleteAnnotation" is published', function() {
it('calls emit("annotationDeleted")', function() {
const ann = {id: 1, $tag: 'tag1'};
const ann = { id: 1, $tag: 'tag1' };
const eventStub = sinon.stub();
options.on('annotationDeleted', eventStub);
createAnnotationSync();
publish('deleteAnnotation', {msg: ann}, function() {});
publish('deleteAnnotation', { msg: ann }, function() {});
assert.calledWith(eventStub, ann);
});
it("calls the 'deleteAnnotation' event's callback function", function(done) {
const ann = {id: 1, $tag: 'tag1'};
const ann = { id: 1, $tag: 'tag1' };
const callback = function(err, ret) {
assert.isNull(err);
assert.deepEqual(ret, {tag: 'tag1', msg: ann});
assert.deepEqual(ret, { tag: 'tag1', msg: ann });
done();
};
createAnnotationSync();
publish('deleteAnnotation', {msg: ann}, callback);
publish('deleteAnnotation', { msg: ann }, callback);
});
it('deletes any existing annotation from its cache before calling emit', function() {
const ann = {id: 1, $tag: 'tag1'};
const ann = { id: 1, $tag: 'tag1' };
const annSync = createAnnotationSync();
annSync.cache.tag1 = ann;
options.emit = function() { assert(!annSync.cache.tag1); };
options.emit = function() {
assert(!annSync.cache.tag1);
};
publish('deleteAnnotation', {msg: ann}, function() {});
publish('deleteAnnotation', { msg: ann }, function() {});
});
it('deletes any existing annotation from its cache', function() {
const ann = {id: 1, $tag: 'tag1'};
const ann = { id: 1, $tag: 'tag1' };
const annSync = createAnnotationSync();
annSync.cache.tag1 = ann;
publish('deleteAnnotation', {msg: ann}, function() {});
publish('deleteAnnotation', { msg: ann }, function() {});
assert(!annSync.cache.tag1);
});
......@@ -93,14 +95,14 @@ describe('AnnotationSync', function() {
context('when "loadAnnotations" is published', function() {
it('calls emit("annotationsLoaded")', function() {
const annotations = [
{id: 1, $tag: 'tag1'},
{id: 2, $tag: 'tag2'},
{id: 3, $tag: 'tag3'},
{ id: 1, $tag: 'tag1' },
{ id: 2, $tag: 'tag2' },
{ id: 3, $tag: 'tag3' },
];
const bodies = [
{msg: annotations[0], tag: annotations[0].$tag},
{msg: annotations[1], tag: annotations[1].$tag},
{msg: annotations[2], tag: annotations[2].$tag},
{ msg: annotations[0], tag: annotations[0].$tag },
{ msg: annotations[1], tag: annotations[1].$tag },
{ msg: annotations[2], tag: annotations[2].$tag },
];
const loadedStub = sinon.stub();
options.on('annotationsLoaded', loadedStub);
......@@ -114,20 +116,23 @@ describe('AnnotationSync', function() {
context('when "beforeAnnotationCreated" is emitted', function() {
it('calls bridge.call() passing the event', function() {
const ann = {id: 1};
const ann = { id: 1 };
createAnnotationSync();
options.emit('beforeAnnotationCreated', ann);
assert.called(fakeBridge.call);
assert.calledWith(
fakeBridge.call, 'beforeCreateAnnotation', {msg: ann, tag: ann.$tag},
sinon.match.func);
fakeBridge.call,
'beforeCreateAnnotation',
{ msg: ann, tag: ann.$tag },
sinon.match.func
);
});
context('if the annotation has a $tag', function() {
it('does not call bridge.call()', function() {
const ann = {id: 1, $tag: 'tag1'};
const ann = { id: 1, $tag: 'tag1' };
createAnnotationSync();
options.emit('beforeAnnotationCreated', ann);
......
......@@ -3,24 +3,23 @@
const events = require('../../shared/bridge-events');
const features = require('../features');
describe('features - annotation layer', function () {
describe('features - annotation layer', function() {
let featureFlagsUpdateHandler;
const initialFeatures = {
feature_on: true,
feature_off: false,
};
const setFeatures = function(features){
const setFeatures = function(features) {
featureFlagsUpdateHandler(features || initialFeatures);
};
beforeEach(function () {
beforeEach(function() {
sinon.stub(console, 'warn');
features.init({
on: function(topic, handler){
if(topic === events.FEATURE_FLAGS_UPDATED){
on: function(topic, handler) {
if (topic === events.FEATURE_FLAGS_UPDATED) {
featureFlagsUpdateHandler = handler;
}
},
......@@ -30,29 +29,28 @@ describe('features - annotation layer', function () {
setFeatures();
});
afterEach(function () {
afterEach(function() {
console.warn.restore();
features.reset();
});
describe('flagEnabled', function () {
it('should retrieve features data', function () {
describe('flagEnabled', function() {
it('should retrieve features data', function() {
assert.equal(features.flagEnabled('feature_on'), true);
assert.equal(features.flagEnabled('feature_off'), false);
});
it('should return false if features have not been loaded', function () {
it('should return false if features have not been loaded', function() {
// simulate feature data not having been loaded yet
features.reset();
assert.equal(features.flagEnabled('feature_on'), false);
});
it('should return false for unknown flags', function () {
it('should return false for unknown flags', function() {
assert.isFalse(features.flagEnabled('unknown_feature'));
});
it('should warn when accessing unknown flags', function () {
it('should warn when accessing unknown flags', function() {
assert.notCalled(console.warn);
assert.isFalse(features.flagEnabled('unknown_feature'));
assert.calledOnce(console.warn);
......
......@@ -16,9 +16,11 @@ function quoteSelector(quote) {
/** Generate an annotation that matches a text quote in a page. */
function annotateQuote(quote) {
return {
target: [{
selector: [quoteSelector(quote)],
}],
target: [
{
selector: [quoteSelector(quote)],
},
],
};
}
......@@ -28,7 +30,9 @@ function annotateQuote(quote) {
* @param {Element} container
*/
function highlightedPhrases(container) {
return Array.from(container.querySelectorAll('.annotator-hl')).map(function (el) {
return Array.from(container.querySelectorAll('.annotator-hl')).map(function(
el
) {
return el.textContent;
});
}
......@@ -44,16 +48,16 @@ function FakeCrossFrame() {
this.sync = sinon.stub();
}
describe('anchoring', function () {
describe('anchoring', function() {
let guest;
let guestConfig;
let container;
before(function () {
guestConfig = {pluginClasses: {CrossFrame: FakeCrossFrame}};
before(function() {
guestConfig = { pluginClasses: { CrossFrame: FakeCrossFrame } };
});
beforeEach(function () {
beforeEach(function() {
sinon.stub(console, 'warn');
container = document.createElement('div');
container.innerHTML = require('./test-page.html');
......@@ -61,43 +65,56 @@ describe('anchoring', function () {
guest = new Guest(container, guestConfig);
});
afterEach(function () {
afterEach(function() {
guest.destroy();
container.parentNode.removeChild(container);
console.warn.restore();
});
unroll('should highlight #tag when annotations are loaded', function (testCase) {
const normalize = function (quotes) {
return quotes.map(function (q) { return simplifyWhitespace(q); });
};
unroll(
'should highlight #tag when annotations are loaded',
function(testCase) {
const normalize = function(quotes) {
return quotes.map(function(q) {
return simplifyWhitespace(q);
});
};
const annotations = testCase.quotes.map(function (q) {
return annotateQuote(q);
});
const annotations = testCase.quotes.map(function(q) {
return annotateQuote(q);
});
const anchored = annotations.map(function (ann) {
return guest.anchor(ann);
});
const anchored = annotations.map(function(ann) {
return guest.anchor(ann);
});
return Promise.all(anchored).then(function () {
const assertFn = testCase.expectFail ? assert.notDeepEqual : assert.deepEqual;
assertFn(normalize(highlightedPhrases(container)),
normalize(testCase.quotes));
});
}, [{
tag: 'a simple quote',
quotes: ['This has not been a scientist\'s war'],
},{
// Known failure with nested annotations that are anchored via quotes
// or positions. See https://github.com/hypothesis/h/pull/3313 and
// https://github.com/hypothesis/h/issues/3278
tag: 'nested quotes',
quotes: [
'This has not been a scientist\'s war;' +
' it has been a war in which all have had a part',
'scientist\'s war',
],
expectFail: true,
}]);
return Promise.all(anchored).then(function() {
const assertFn = testCase.expectFail
? assert.notDeepEqual
: assert.deepEqual;
assertFn(
normalize(highlightedPhrases(container)),
normalize(testCase.quotes)
);
});
},
[
{
tag: 'a simple quote',
quotes: ["This has not been a scientist's war"],
},
{
// Known failure with nested annotations that are anchored via quotes
// or positions. See https://github.com/hypothesis/h/pull/3313 and
// https://github.com/hypothesis/h/issues/3278
tag: 'nested quotes',
quotes: [
"This has not been a scientist's war;" +
' it has been a war in which all have had a part',
"scientist's war",
],
expectFail: true,
},
]
);
});
......@@ -23,11 +23,11 @@ function roundCoords(rect) {
};
}
describe('annotator.range-util', function () {
describe('annotator.range-util', function() {
let selection;
let testNode;
beforeEach(function () {
beforeEach(function() {
selection = window.getSelection();
selection.collapse(null);
......@@ -36,7 +36,7 @@ describe('annotator.range-util', function () {
document.body.appendChild(testNode);
});
afterEach(function () {
afterEach(function() {
testNode.parentElement.removeChild(testNode);
});
......@@ -46,70 +46,78 @@ describe('annotator.range-util', function () {
selection.addRange(range);
}
describe('#isNodeInRange', function () {
it('is true for a node in the range', function () {
describe('#isNodeInRange', function() {
it('is true for a node in the range', function() {
const rng = createRange(testNode, 0, 1);
assert.equal(rangeUtil.isNodeInRange(rng, testNode.firstChild), true);
});
it('is false for a node before the range', function () {
it('is false for a node before the range', function() {
testNode.innerHTML = 'one <b>two</b> three';
const rng = createRange(testNode, 1, 2);
assert.equal(rangeUtil.isNodeInRange(rng, testNode.firstChild), false);
});
it('is false for a node after the range', function () {
it('is false for a node after the range', function() {
testNode.innerHTML = 'one <b>two</b> three';
const rng = createRange(testNode, 1, 2);
assert.equal(rangeUtil.isNodeInRange(rng, testNode.childNodes.item(2)), false);
assert.equal(
rangeUtil.isNodeInRange(rng, testNode.childNodes.item(2)),
false
);
});
});
describe('#getTextBoundingBoxes', function () {
it('gets the bounding box of a range in a text node', function () {
describe('#getTextBoundingBoxes', function() {
it('gets the bounding box of a range in a text node', function() {
testNode.innerHTML = 'plain text';
const rng = createRange(testNode.firstChild, 0, 5);
const boxes = rangeUtil.getTextBoundingBoxes(rng);
assert.ok(boxes.length);
});
it('gets the bounding box of a range containing a text node', function () {
it('gets the bounding box of a range containing a text node', function() {
testNode.innerHTML = 'plain text';
const rng = createRange(testNode, 0, 1);
const boxes = rangeUtil.getTextBoundingBoxes(rng);
assert.match(boxes, [sinon.match({
left: sinon.match.number,
top: sinon.match.number,
width: sinon.match.number,
height: sinon.match.number,
bottom: sinon.match.number,
right: sinon.match.number,
})]);
assert.match(boxes, [
sinon.match({
left: sinon.match.number,
top: sinon.match.number,
width: sinon.match.number,
height: sinon.match.number,
bottom: sinon.match.number,
right: sinon.match.number,
}),
]);
});
it('returns the bounding box in viewport coordinates', function () {
it('returns the bounding box in viewport coordinates', function() {
testNode.innerHTML = 'plain text';
const rng = createRange(testNode, 0, 1);
const [rect] = rangeUtil.getTextBoundingBoxes(rng);
assert.deepEqual(roundCoords(rect), roundCoords(testNode.getBoundingClientRect()));
assert.deepEqual(
roundCoords(rect),
roundCoords(testNode.getBoundingClientRect())
);
});
});
describe('#selectionFocusRect', function () {
it('returns null if the selection is empty', function () {
describe('#selectionFocusRect', function() {
it('returns null if the selection is empty', function() {
assert.isNull(rangeUtil.selectionFocusRect(selection));
});
it('returns a point if the selection is not empty', function () {
it('returns a point if the selection is not empty', function() {
selectNode(testNode);
assert.ok(rangeUtil.selectionFocusRect(selection));
});
it('returns the first line\'s rect if the selection is backwards', function () {
it("returns the first line's rect if the selection is backwards", function() {
selectNode(testNode);
selection.collapseToEnd();
selection.extend(testNode, 0);
......@@ -118,11 +126,14 @@ describe('annotator.range-util', function () {
assert.equal(rect.top, testNode.offsetTop);
});
it('returns the last line\'s rect if the selection is forwards', function () {
it("returns the last line's rect if the selection is forwards", function() {
selectNode(testNode);
const rect = rangeUtil.selectionFocusRect(selection);
assert.equal(rect.left, testNode.offsetLeft);
assert.equal(rect.top + rect.height, testNode.offsetTop + testNode.offsetHeight);
assert.equal(
rect.top + rect.height,
testNode.offsetTop + testNode.offsetHeight
);
});
});
});
......@@ -9,91 +9,95 @@ function FakeDocument() {
const listeners = {};
return {
getSelection: function () {
getSelection: function() {
return this.selection;
},
addEventListener: function (name, listener) {
addEventListener: function(name, listener) {
listeners[name] = (listeners[name] || []).concat(listener);
},
removeEventListener: function (name, listener) {
listeners[name] = listeners[name].filter(function (lis) {
removeEventListener: function(name, listener) {
listeners[name] = listeners[name].filter(function(lis) {
return lis !== listener;
});
},
dispatchEvent: function (event) {
listeners[event.type].forEach(function (fn) { fn(event); });
dispatchEvent: function(event) {
listeners[event.type].forEach(function(fn) {
fn(event);
});
},
};
}
describe('selections', function () {
describe('selections', function() {
let clock;
let fakeDocument;
let range;
let rangeSub;
let onSelectionChanged;
beforeEach(function () {
beforeEach(function() {
clock = sinon.useFakeTimers();
fakeDocument = new FakeDocument();
onSelectionChanged = sinon.stub();
// Subscribe to selection changes, ignoring the initial event
const ranges = observable.drop(selections(fakeDocument), 1);
rangeSub = ranges.subscribe({next: onSelectionChanged});
rangeSub = ranges.subscribe({ next: onSelectionChanged });
range = {};
fakeDocument.selection = {
rangeCount: 1,
getRangeAt: function (index) {
getRangeAt: function(index) {
return index === 0 ? range : null;
},
};
});
afterEach(function () {
afterEach(function() {
rangeSub.unsubscribe();
clock.restore();
});
unroll('emits the selected range when #event occurs', function (testCase) {
fakeDocument.dispatchEvent({type: testCase.event});
clock.tick(testCase.delay);
assert.calledWith(onSelectionChanged, range);
}, [
{event: 'mouseup', delay: 20},
]);
unroll(
'emits the selected range when #event occurs',
function(testCase) {
fakeDocument.dispatchEvent({ type: testCase.event });
clock.tick(testCase.delay);
assert.calledWith(onSelectionChanged, range);
},
[{ event: 'mouseup', delay: 20 }]
);
it('emits an event if there is a selection at the initial subscription', function () {
it('emits an event if there is a selection at the initial subscription', function() {
const onInitialSelection = sinon.stub();
const ranges = selections(fakeDocument);
const sub = ranges.subscribe({next: onInitialSelection});
const sub = ranges.subscribe({ next: onInitialSelection });
clock.tick(1);
assert.called(onInitialSelection);
sub.unsubscribe();
});
describe('when the selection changes', function () {
it('emits a selection if the mouse is not down', function () {
fakeDocument.dispatchEvent({type: 'selectionchange'});
describe('when the selection changes', function() {
it('emits a selection if the mouse is not down', function() {
fakeDocument.dispatchEvent({ type: 'selectionchange' });
clock.tick(200);
assert.calledWith(onSelectionChanged, range);
});
it('does not emit a selection if the mouse is down', function () {
fakeDocument.dispatchEvent({type: 'mousedown'});
fakeDocument.dispatchEvent({type: 'selectionchange'});
it('does not emit a selection if the mouse is down', function() {
fakeDocument.dispatchEvent({ type: 'mousedown' });
fakeDocument.dispatchEvent({ type: 'selectionchange' });
clock.tick(200);
assert.notCalled(onSelectionChanged);
});
it('does not emit a selection until there is a pause since the last change', function () {
fakeDocument.dispatchEvent({type: 'selectionchange'});
it('does not emit a selection until there is a pause since the last change', function() {
fakeDocument.dispatchEvent({ type: 'selectionchange' });
clock.tick(90);
fakeDocument.dispatchEvent({type: 'selectionchange'});
fakeDocument.dispatchEvent({ type: 'selectionchange' });
clock.tick(90);
assert.notCalled(onSelectionChanged);
clock.tick(20);
......
......@@ -2,11 +2,11 @@
const sidebarTrigger = require('../sidebar-trigger');
describe('sidebarTrigger', function () {
describe('sidebarTrigger', function() {
let triggerEl1;
let triggerEl2;
beforeEach(function () {
beforeEach(function() {
triggerEl1 = document.createElement('button');
triggerEl1.setAttribute('data-hypothesis-trigger', '');
document.body.appendChild(triggerEl1);
......@@ -16,7 +16,7 @@ describe('sidebarTrigger', function () {
document.body.appendChild(triggerEl2);
});
it('calls the show callback which a trigger button is clicked', function () {
it('calls the show callback which a trigger button is clicked', function() {
const fakeShowFn = sinon.stub();
sidebarTrigger(document, fakeShowFn);
......
......@@ -8,24 +8,24 @@
const Observable = require('zen-observable');
/**
* Returns an observable of events emitted by a DOM event source
* (eg. an Element, Document or Window).
*
* @param {EventTarget} src - The event source.
* @param {Array<string>} eventNames - List of events to subscribe to
*/
* Returns an observable of events emitted by a DOM event source
* (eg. an Element, Document or Window).
*
* @param {EventTarget} src - The event source.
* @param {Array<string>} eventNames - List of events to subscribe to
*/
function listen(src, eventNames) {
return new Observable(function (observer) {
const onNext = function (event) {
return new Observable(function(observer) {
const onNext = function(event) {
observer.next(event);
};
eventNames.forEach(function (event) {
eventNames.forEach(function(event) {
src.addEventListener(event, onNext);
});
return function () {
eventNames.forEach(function (event) {
return function() {
eventNames.forEach(function(event) {
src.removeEventListener(event, onNext);
});
};
......@@ -36,18 +36,20 @@ function listen(src, eventNames) {
* Delay events from a source Observable by `delay` ms.
*/
function delay(delay, src) {
return new Observable(function (obs) {
return new Observable(function(obs) {
let timeouts = [];
const sub = src.subscribe({
next: function (value) {
const t = setTimeout(function () {
timeouts = timeouts.filter(function (other) { return other !== t; });
next: function(value) {
const t = setTimeout(function() {
timeouts = timeouts.filter(function(other) {
return other !== t;
});
obs.next(value);
}, delay);
timeouts.push(t);
},
});
return function () {
return function() {
timeouts.forEach(clearTimeout);
sub.unsubscribe();
};
......@@ -55,15 +57,15 @@ function delay(delay, src) {
}
/**
* Buffers events from a source Observable, waiting for a pause of `delay`
* ms with no events before emitting the last value from `src`.
*
* @param {number} delay
* @param {Observable<T>} src
* @return {Observable<T>}
*/
* Buffers events from a source Observable, waiting for a pause of `delay`
* ms with no events before emitting the last value from `src`.
*
* @param {number} delay
* @param {Observable<T>} src
* @return {Observable<T>}
*/
function buffer(delay, src) {
return new Observable(function (obs) {
return new Observable(function(obs) {
let lastValue;
let timeout;
......@@ -72,14 +74,14 @@ function buffer(delay, src) {
}
const sub = src.subscribe({
next: function (value) {
next: function(value) {
lastValue = value;
clearTimeout(timeout);
timeout = setTimeout(onNext, delay);
},
});
return function () {
return function() {
sub.unsubscribe();
clearTimeout(timeout);
};
......@@ -87,23 +89,23 @@ function buffer(delay, src) {
}
/**
* Merges multiple streams of values into a single stream.
*
* @param {Array<Observable>} sources
* @return Observable
*/
* Merges multiple streams of values into a single stream.
*
* @param {Array<Observable>} sources
* @return Observable
*/
function merge(sources) {
return new Observable(function (obs) {
const subs = sources.map(function (src) {
return new Observable(function(obs) {
const subs = sources.map(function(src) {
return src.subscribe({
next: function (value) {
next: function(value) {
obs.next(value);
},
});
});
return function () {
subs.forEach(function (sub) {
return function() {
subs.forEach(function(sub) {
sub.unsubscribe();
});
};
......@@ -113,7 +115,7 @@ function merge(sources) {
/** Drop the first `n` events from the `src` Observable. */
function drop(src, n) {
let count = 0;
return src.filter(function () {
return src.filter(function() {
++count;
return count > n;
});
......
......@@ -2,42 +2,48 @@
const frameUtil = require('../frame-util');
describe('annotator.util.frame-util', function () {
describe('findFrames', function () {
describe('annotator.util.frame-util', function() {
describe('findFrames', function() {
let container;
const _addFrameToContainer = (options={})=>{
const _addFrameToContainer = (options = {}) => {
const frame = document.createElement('iframe');
frame.setAttribute('enable-annotation', '');
frame.className = options.className || '';
frame.style.height = `${(options.height || 150)}px`;
frame.style.width = `${(options.width || 150)}px`;
frame.style.height = `${options.height || 150}px`;
frame.style.width = `${options.width || 150}px`;
container.appendChild(frame);
return frame;
};
beforeEach(function () {
beforeEach(function() {
container = document.createElement('div');
document.body.appendChild(container);
});
afterEach(function () {
afterEach(function() {
container.remove();
});
it('should return valid frames', function () {
it('should return valid frames', function() {
let foundFrames = frameUtil.findFrames(container);
assert.lengthOf(foundFrames, 0, 'no frames appended so none should be found');
assert.lengthOf(
foundFrames,
0,
'no frames appended so none should be found'
);
const frame1 = _addFrameToContainer();
const frame2 = _addFrameToContainer();
foundFrames = frameUtil.findFrames(container);
assert.deepEqual(foundFrames, [frame1, frame2], 'appended frames should be found');
assert.deepEqual(
foundFrames,
[frame1, frame2],
'appended frames should be found'
);
});
it('should not return frames that have not opted into annotation', () => {
......@@ -49,13 +55,16 @@ describe('annotator.util.frame-util', function () {
assert.lengthOf(foundFrames, 0);
});
it('should not return the Hypothesis sidebar', function () {
_addFrameToContainer({className: 'h-sidebar-iframe other-class-too'});
it('should not return the Hypothesis sidebar', function() {
_addFrameToContainer({ className: 'h-sidebar-iframe other-class-too' });
const foundFrames = frameUtil.findFrames(container);
assert.lengthOf(foundFrames, 0, 'frames with hypothesis className should not be found');
assert.lengthOf(
foundFrames,
0,
'frames with hypothesis className should not be found'
);
});
});
});
......@@ -2,22 +2,22 @@
const observable = require('../observable');
describe('observable', function () {
describe('delay()', function () {
describe('observable', function() {
describe('delay()', function() {
let clock;
beforeEach(function () {
beforeEach(function() {
clock = sinon.useFakeTimers();
});
afterEach(function () {
afterEach(function() {
clock.restore();
});
it('defers events', function () {
it('defers events', function() {
const received = [];
const obs = observable.delay(50, observable.Observable.of('foo'));
obs.forEach(function (v) {
obs.forEach(function(v) {
received.push(v);
});
assert.deepEqual(received, []);
......@@ -25,14 +25,14 @@ describe('observable', function () {
assert.deepEqual(received, ['foo']);
});
it('delivers events in sequence', function () {
it('delivers events in sequence', function() {
const received = [];
const obs = observable.delay(10, observable.Observable.of(1,2));
obs.forEach(function (v) {
const obs = observable.delay(10, observable.Observable.of(1, 2));
obs.forEach(function(v) {
received.push(v);
});
clock.tick(20);
assert.deepEqual(received, [1,2]);
assert.deepEqual(received, [1, 2]);
});
});
});
......@@ -25,10 +25,7 @@ describe('annotator.util.url', () => {
assert.equal(normalizeURI(url), 'http://example.com/wibble');
});
[
'file:///Users/jane/article.pdf',
'doi:10.1234/4567',
].forEach(url => {
['file:///Users/jane/article.pdf', 'doi:10.1234/4567'].forEach(url => {
it('does not modify absolute non-HTTP/HTTPS URLs', () => {
assert.equal(normalizeURI(url), url);
});
......
......@@ -20,7 +20,7 @@ function injectScript(doc, src) {
}
function injectAssets(doc, config, assets) {
assets.forEach(function (path) {
assets.forEach(function(path) {
const url = config.assetRoot + 'build/' + config.manifest[path];
if (url.match(/\.css/)) {
injectStylesheet(doc, url);
......@@ -37,7 +37,9 @@ function injectAssets(doc, config, assets) {
*/
function bootHypothesisClient(doc, config) {
// Detect presence of Hypothesis in the page
const appLinkEl = doc.querySelector('link[type="application/annotator+html"]');
const appLinkEl = doc.querySelector(
'link[type="application/annotator+html"]'
);
if (appLinkEl) {
return;
}
......
......@@ -2,15 +2,15 @@
const boot = require('../boot');
describe('bootstrap', function () {
describe('bootstrap', function() {
let iframe;
beforeEach(function () {
beforeEach(function() {
iframe = document.createElement('iframe');
document.body.appendChild(iframe);
});
afterEach(function () {
afterEach(function() {
iframe.remove();
});
......@@ -40,7 +40,7 @@ describe('bootstrap', function () {
'styles/sidebar.css',
];
const manifest = assetNames.reduce(function (manifest, path) {
const manifest = assetNames.reduce(function(manifest, path) {
const url = path.replace(/\.([a-z]+)$/, '.1234.$1');
manifest[path] = url;
return manifest;
......@@ -54,20 +54,23 @@ describe('bootstrap', function () {
}
function findAssets(doc_) {
const scripts = Array.from(doc_.querySelectorAll('script')).map(function (el) {
const scripts = Array.from(doc_.querySelectorAll('script')).map(function(
el
) {
return el.src;
});
const styles = Array.from(doc_.querySelectorAll('link[rel="stylesheet"]'))
.map(function (el) {
return el.href;
});
const styles = Array.from(
doc_.querySelectorAll('link[rel="stylesheet"]')
).map(function(el) {
return el.href;
});
return scripts.concat(styles).sort();
}
context('in the host page', function () {
it('loads assets for the annotation layer', function () {
context('in the host page', function() {
it('loads assets for the annotation layer', function() {
runBoot();
const expectedAssets = [
'scripts/annotator.bundle.1234.js',
......@@ -76,23 +79,24 @@ describe('bootstrap', function () {
'styles/annotator.1234.css',
'styles/icomoon.1234.css',
'styles/pdfjs-overrides.1234.css',
].map(function (url) {
].map(function(url) {
return 'https://marginal.ly/client/build/' + url;
});
assert.deepEqual(findAssets(iframe.contentDocument), expectedAssets);
});
it('creates the link to the sidebar iframe', function () {
it('creates the link to the sidebar iframe', function() {
runBoot();
const sidebarAppLink = iframe.contentDocument
.querySelector('link[type="application/annotator+html"]');
const sidebarAppLink = iframe.contentDocument.querySelector(
'link[type="application/annotator+html"]'
);
assert.ok(sidebarAppLink);
assert.equal(sidebarAppLink.href, 'https://marginal.ly/app.html');
});
it('does nothing if Hypothesis is already loaded in the document', function () {
it('does nothing if Hypothesis is already loaded in the document', function() {
const link = iframe.contentDocument.createElement('link');
link.type = 'application/annotator+html';
iframe.contentDocument.head.appendChild(link);
......@@ -103,19 +107,19 @@ describe('bootstrap', function () {
});
});
context('in the sidebar application', function () {
context('in the sidebar application', function() {
let appRootElement;
beforeEach(function () {
beforeEach(function() {
appRootElement = iframe.contentDocument.createElement('hypothesis-app');
iframe.contentDocument.body.appendChild(appRootElement);
});
afterEach(function () {
afterEach(function() {
appRootElement.remove();
});
it('loads assets for the sidebar application', function () {
it('loads assets for the sidebar application', function() {
runBoot();
const expectedAssets = [
'scripts/angular.bundle.1234.js',
......@@ -130,7 +134,7 @@ describe('bootstrap', function () {
'styles/icomoon.1234.css',
'styles/katex.min.1234.css',
'styles/sidebar.1234.css',
].map(function (url) {
].map(function(url) {
return 'https://marginal.ly/client/build/' + url;
});
......
......@@ -6,18 +6,12 @@ const path = require('path');
module.exports = function(config) {
config.set({
// base path that will be used to resolve all patterns (eg. files, exclude)
basePath: './',
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: [
'browserify',
'mocha',
'chai',
'sinon',
],
frameworks: ['browserify', 'mocha', 'chai', 'sinon'],
// list of files / patterns to load in the browser
files: [
......@@ -35,16 +29,30 @@ module.exports = function(config) {
// watchify
// Unit tests
{ pattern: 'annotator/**/*-test.coffee', watched: false, included: true, served: true },
{ pattern: '**/test/*-test.js', watched: false, included: true, served: true },
{
pattern: 'annotator/**/*-test.coffee',
watched: false,
included: true,
served: true,
},
{
pattern: '**/test/*-test.js',
watched: false,
included: true,
served: true,
},
// Integration tests
{ pattern: '**/integration/*-test.js', watched: false, included: true, served: true },
{
pattern: '**/integration/*-test.js',
watched: false,
included: true,
served: true,
},
],
// list of files to exclude
exclude: [
],
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
......@@ -59,22 +67,28 @@ module.exports = function(config) {
browserify: {
debug: true,
extensions: ['.coffee'],
configure: function (bundle) {
configure: function(bundle) {
bundle.plugin('proxyquire-universal');
},
transform: [
'coffeeify',
['babelify', {
// The transpiled CoffeeScript is fed through Babelify to add
// code coverage instrumentation for Istanbul.
extensions: ['.js', '.coffee'],
plugins: [
['babel-plugin-istanbul', {
'exclude': ['**/test/**/*.{coffee,js}'],
}],
],
}],
[
'babelify',
{
// The transpiled CoffeeScript is fed through Babelify to add
// code coverage instrumentation for Istanbul.
extensions: ['.js', '.coffee'],
plugins: [
[
'babel-plugin-istanbul',
{
exclude: ['**/test/**/*.{coffee,js}'],
},
],
],
},
],
],
},
......
......@@ -45,7 +45,6 @@ module.exports = {
*/
SIGNUP_REQUESTED: 'signupRequested',
// Events that the annotator sends to the sidebar
// ----------------------------------------------
};
......@@ -21,8 +21,7 @@ class Bridge {
* This removes the event listeners for messages arriving from other windows.
*/
destroy() {
Array.from(this.links).map((link) =>
link.channel.destroy());
Array.from(this.links).map(link => link.channel.destroy());
}
/**
......@@ -41,9 +40,11 @@ class Bridge {
let connected = false;
const ready = () => {
if (connected) { return; }
if (connected) {
return;
}
connected = true;
Array.from(this.onConnectListeners).forEach((cb) =>
Array.from(this.onConnectListeners).forEach(cb =>
cb.call(null, channel, source)
);
};
......@@ -55,7 +56,7 @@ class Bridge {
}
};
const listeners = extend({connect}, this.channelListeners);
const listeners = extend({ connect }, this.channelListeners);
// Set up a channel
channel = new RPC(window, source, origin, listeners);
......@@ -82,7 +83,7 @@ class Bridge {
*/
call(method, ...args) {
let cb;
if (typeof(args[args.length - 1]) === 'function') {
if (typeof args[args.length - 1] === 'function') {
cb = args[args.length - 1];
args = args.slice(0, -1);
}
......@@ -90,18 +91,27 @@ class Bridge {
const _makeDestroyFn = c => {
return error => {
c.destroy();
this.links = (Array.from(this.links).filter((l) => l.channel !== c).map((l) => l));
this.links = Array.from(this.links)
.filter(l => l.channel !== c)
.map(l => l);
throw error;
};
};
const promises = this.links.map(function(l) {
const p = new Promise(function(resolve, reject) {
const timeout = setTimeout((() => resolve(null)), 1000);
const timeout = setTimeout(() => resolve(null), 1000);
try {
return l.channel.call(method, ...Array.from(args), function(err, result) {
return l.channel.call(method, ...Array.from(args), function(
err,
result
) {
clearTimeout(timeout);
if (err) { return reject(err); } else { return resolve(result); }
if (err) {
return reject(err);
} else {
return resolve(result);
}
});
} catch (error) {
const err = error;
......
......@@ -37,7 +37,7 @@ class Discovery {
* @param {Window} target
* @param {Object} options
*/
constructor(target, options={}) {
constructor(target, options = {}) {
/** The window to send and listen for messages with. */
this.target = target;
......@@ -80,7 +80,9 @@ class Discovery {
*/
startDiscovery(onDiscovery) {
if (this.onDiscovery) {
throw new Error('Discovery is already in progress. Call stopDiscovery() first');
throw new Error(
'Discovery is already in progress. Call stopDiscovery() first'
);
}
this.onDiscovery = onDiscovery;
......@@ -142,26 +144,32 @@ class Discovery {
// When sending messages to or from a Firefox WebExtension, current
// versions of Firefox have a bug that causes the origin check to fail even
// though the target and actual origins of the message match.
if (origin === 'null' || origin.match('moz-extension:') ||
window.location.protocol === 'moz-extension:') {
if (
origin === 'null' ||
origin.match('moz-extension:') ||
window.location.protocol === 'moz-extension:'
) {
origin = '*';
}
// Check if this is a recognized message from a `Discovery` instance in
// another frame.
const match = (
const match =
typeof data === 'string' &&
data.match(/^__cross_frame_dhcp_(discovery|offer|request|ack)(?::(\d+))?$/)
);
data.match(
/^__cross_frame_dhcp_(discovery|offer|request|ack)(?::(\d+))?$/
);
if (!match) {
return;
}
// Handle the message, and send a response back to the original frame if
// appropriate.
let [ , messageType, messageToken ] = match;
let [, messageType, messageToken] = match;
const { reply, discovered, token } = this._processMessage(
messageType, messageToken, origin
messageType,
messageToken,
origin
);
if (reply) {
......@@ -195,7 +203,9 @@ class Discovery {
}
} else {
// Handle message as a client frame.
if (messageType === 'offer') { // eslint-disable-line no-lonely-if
// eslint-disable-next-line no-lonely-if
if (messageType === 'offer') {
// eslint-disable-line no-lonely-if
if (!this.requestInProgress) {
this.requestInProgress = true;
reply = 'request';
......@@ -214,7 +224,9 @@ class Discovery {
* and a server.
*/
generateToken() {
return Math.random().toString().replace(/\D/g, '');
return Math.random()
.toString()
.replace(/\D/g, '');
}
}
......
......@@ -33,83 +33,85 @@ const VERSION = '1.0.0';
module.exports = RPC;
function RPC (src, dst, origin, methods) {
if (!(this instanceof RPC)) return new RPC(src, dst, origin, methods);
const self = this;
this.src = src;
this.dst = dst;
if (origin === '*') {
this.origin = '*';
}
else {
const uorigin = new URL(origin);
this.origin = uorigin.protocol + '//' + uorigin.host;
}
this._sequence = 0;
this._callbacks = {};
this._onmessage = function (ev) {
if (self._destroyed) return;
if (self.dst !== ev.source) return;
if (self.origin !== '*' && ev.origin !== self.origin) return;
if (!ev.data || typeof ev.data !== 'object') return;
if (ev.data.protocol !== 'frame-rpc') return;
if (!Array.isArray(ev.data.arguments)) return;
self._handle(ev.data);
};
this.src.addEventListener('message', this._onmessage);
this._methods = (typeof methods === 'function'
? methods(this)
: methods
) || {};
function RPC(src, dst, origin, methods) {
if (!(this instanceof RPC)) return new RPC(src, dst, origin, methods);
const self = this;
this.src = src;
this.dst = dst;
if (origin === '*') {
this.origin = '*';
} else {
const uorigin = new URL(origin);
this.origin = uorigin.protocol + '//' + uorigin.host;
}
this._sequence = 0;
this._callbacks = {};
this._onmessage = function(ev) {
if (self._destroyed) return;
if (self.dst !== ev.source) return;
if (self.origin !== '*' && ev.origin !== self.origin) return;
if (!ev.data || typeof ev.data !== 'object') return;
if (ev.data.protocol !== 'frame-rpc') return;
if (!Array.isArray(ev.data.arguments)) return;
self._handle(ev.data);
};
this.src.addEventListener('message', this._onmessage);
this._methods =
(typeof methods === 'function' ? methods(this) : methods) || {};
}
RPC.prototype.destroy = function () {
this._destroyed = true;
this.src.removeEventListener('message', this._onmessage);
RPC.prototype.destroy = function() {
this._destroyed = true;
this.src.removeEventListener('message', this._onmessage);
};
RPC.prototype.call = function (method) {
const args = [].slice.call(arguments, 1);
return this.apply(method, args);
RPC.prototype.call = function(method) {
const args = [].slice.call(arguments, 1);
return this.apply(method, args);
};
RPC.prototype.apply = function (method, args) {
if (this._destroyed) return;
const seq = this._sequence ++;
if (typeof args[args.length - 1] === 'function') {
this._callbacks[seq] = args[args.length - 1];
args = args.slice(0, -1);
}
this.dst.postMessage({
protocol: 'frame-rpc',
version: VERSION,
sequence: seq,
method: method,
arguments: args
}, this.origin);
RPC.prototype.apply = function(method, args) {
if (this._destroyed) return;
const seq = this._sequence++;
if (typeof args[args.length - 1] === 'function') {
this._callbacks[seq] = args[args.length - 1];
args = args.slice(0, -1);
}
this.dst.postMessage(
{
protocol: 'frame-rpc',
version: VERSION,
sequence: seq,
method: method,
arguments: args,
},
this.origin
);
};
RPC.prototype._handle = function (msg) {
const self = this;
if (self._destroyed) return;
if (msg.hasOwnProperty('method')) {
if (!this._methods.hasOwnProperty(msg.method)) return;
const args = msg.arguments.concat(function () {
self.dst.postMessage({
protocol: 'frame-rpc',
version: VERSION,
response: msg.sequence,
arguments: [].slice.call(arguments)
}, self.origin);
});
this._methods[msg.method].apply(this._methods, args);
}
else if (msg.hasOwnProperty('response')) {
const cb = this._callbacks[msg.response];
delete this._callbacks[msg.response];
if (cb) cb.apply(null, msg.arguments);
}
RPC.prototype._handle = function(msg) {
const self = this;
if (self._destroyed) return;
if (msg.hasOwnProperty('method')) {
if (!this._methods.hasOwnProperty(msg.method)) return;
const args = msg.arguments.concat(function() {
self.dst.postMessage(
{
protocol: 'frame-rpc',
version: VERSION,
response: msg.sequence,
arguments: [].slice.call(arguments),
},
self.origin
);
});
this._methods[msg.method].apply(this._methods, args);
} else if (msg.hasOwnProperty('response')) {
const cb = this._callbacks[msg.response];
delete this._callbacks[msg.response];
if (cb) cb.apply(null, msg.arguments);
}
};
......@@ -29,15 +29,19 @@ function assign(dest, src) {
*/
function jsonConfigsFrom(document) {
const config = {};
const settingsElements =
document.querySelectorAll('script.js-hypothesis-config');
const settingsElements = document.querySelectorAll(
'script.js-hypothesis-config'
);
for (let i=0; i < settingsElements.length; i++) {
for (let i = 0; i < settingsElements.length; i++) {
let settings;
try {
settings = JSON.parse(settingsElements[i].textContent);
} catch (err) {
console.warn('Could not parse settings from js-hypothesis-config tags', err);
console.warn(
'Could not parse settings from js-hypothesis-config tags',
err
);
settings = {};
}
assign(config, settings);
......
......@@ -12,7 +12,8 @@ describe('shared.bridge', function() {
beforeEach(() => {
bridge = new Bridge();
createChannel = () => bridge.createChannel(fakeWindow, 'http://example.com', 'TOKEN');
createChannel = () =>
bridge.createChannel(fakeWindow, 'http://example.com', 'TOKEN');
fakeWindow = {
postMessage: sandbox.stub(),
......@@ -34,7 +35,11 @@ describe('shared.bridge', function() {
it('adds the channel to the .links property', function() {
const channel = createChannel();
assert.isTrue(bridge.links.some(link => (link.channel === channel) && (link.window === fakeWindow)));
assert.isTrue(
bridge.links.some(
link => link.channel === channel && link.window === fakeWindow
)
);
});
it('registers any existing listeners on the channel', function() {
......@@ -71,7 +76,11 @@ describe('shared.bridge', function() {
it('calls a callback when all channels return successfully', function(done) {
const channel1 = createChannel();
const channel2 = bridge.createChannel(fakeWindow, 'http://example.com', 'NEKOT');
const channel2 = bridge.createChannel(
fakeWindow,
'http://example.com',
'NEKOT'
);
sandbox.stub(channel1, 'call').yields(null, 'result1');
sandbox.stub(channel2, 'call').yields(null, 'result2');
......@@ -87,7 +96,11 @@ describe('shared.bridge', function() {
it('calls a callback with an error when a channels fails', function(done) {
const error = new Error('Uh oh');
const channel1 = createChannel();
const channel2 = bridge.createChannel(fakeWindow, 'http://example.com', 'NEKOT');
const channel2 = bridge.createChannel(
fakeWindow,
'http://example.com',
'NEKOT'
);
sandbox.stub(channel1, 'call').throws(error);
sandbox.stub(channel2, 'call').yields(null, 'result2');
......@@ -161,8 +174,7 @@ describe('shared.bridge', function() {
bridge.on('message1', sandbox.spy());
bridge.off('message1');
assert.isUndefined(bridge.channelListeners.message1);
})
);
}));
describe('#onConnect', function() {
it('adds a callback that is called when a channel is connected', function(done) {
......@@ -196,7 +208,9 @@ describe('shared.bridge', function() {
const callback = (c, s) => {
assert.strictEqual(c, channel);
assert.strictEqual(s, fakeWindow);
if (++callbackCount === 2) { done(); }
if (++callbackCount === 2) {
done();
}
};
const data = {
......@@ -220,8 +234,16 @@ describe('shared.bridge', function() {
describe('#destroy', () =>
it('destroys all opened channels', function() {
const channel1 = bridge.createChannel(fakeWindow, 'http://example.com', 'foo');
const channel2 = bridge.createChannel(fakeWindow, 'http://example.com', 'bar');
const channel1 = bridge.createChannel(
fakeWindow,
'http://example.com',
'foo'
);
const channel2 = bridge.createChannel(
fakeWindow,
'http://example.com',
'bar'
);
sinon.spy(channel1, 'destroy');
sinon.spy(channel2, 'destroy');
......@@ -229,6 +251,5 @@ describe('shared.bridge', function() {
assert.called(channel1.destroy);
assert.called(channel2.destroy);
})
);
}));
});
......@@ -31,7 +31,12 @@ describe('shared/discovery', () => {
it('adds a "message" listener to the window object', () => {
const discovery = new Discovery(fakeTopWindow);
discovery.startDiscovery(sinon.stub());
assert.calledWith(fakeTopWindow.addEventListener, 'message', sinon.match.func, false);
assert.calledWith(
fakeTopWindow.addEventListener,
'message',
sinon.match.func,
false
);
});
});
......@@ -44,11 +49,18 @@ describe('shared/discovery', () => {
it('sends "offer" messages to every frame in the current tab', () => {
server.startDiscovery(sinon.stub());
assert.calledWith(fakeTopWindow.postMessage, '__cross_frame_dhcp_offer', '*');
assert.calledWith(
fakeTopWindow.postMessage,
'__cross_frame_dhcp_offer',
'*'
);
});
it('allows the origin to be provided', () => {
server = new Discovery(fakeFrameWindow, { server: true, origin: 'https://example.com' });
server = new Discovery(fakeFrameWindow, {
server: true,
origin: 'https://example.com',
});
server.startDiscovery(sinon.stub());
assert.calledWith(
fakeTopWindow.postMessage,
......@@ -88,7 +100,12 @@ describe('shared/discovery', () => {
server.startDiscovery(onDiscovery);
assert.calledWith(onDiscovery, fakeTopWindow, 'https://top.com', sinon.match(/\d+/));
assert.calledWith(
onDiscovery,
fakeTopWindow,
'https://top.com',
sinon.match(/\d+/)
);
});
it('raises an error if it receives an event from another server', () => {
......@@ -113,7 +130,11 @@ describe('shared/discovery', () => {
it('sends out a discovery message to every frame', () => {
client.startDiscovery(sinon.stub());
assert.calledWith(fakeFrameWindow.postMessage, '__cross_frame_dhcp_discovery', '*');
assert.calledWith(
fakeFrameWindow.postMessage,
'__cross_frame_dhcp_discovery',
'*'
);
});
it('does not send the message to itself', () => {
......@@ -154,7 +175,9 @@ describe('shared/discovery', () => {
// `postMessage` should be called first for discovery, then for offer.
assert.calledTwice(fakeFrameWindow.postMessage);
const lastCall = fakeFrameWindow.postMessage.lastCall;
assert.isTrue(lastCall.notCalledWith(sinon.match.string, 'https://iframe2.com'));
assert.isTrue(
lastCall.notCalledWith(sinon.match.string, 'https://iframe2.com')
);
});
it('does respond to an "offer" if a previous "request" has completed', () => {
......@@ -193,7 +216,12 @@ describe('shared/discovery', () => {
client.startDiscovery(onDiscovery);
assert.calledWith(onDiscovery, fakeFrameWindow, 'https://iframe.com', '1234');
assert.calledWith(
onDiscovery,
fakeFrameWindow,
'https://iframe.com',
'1234'
);
});
});
......@@ -205,7 +233,11 @@ describe('shared/discovery', () => {
discovery.stopDiscovery();
const handler = fakeFrameWindow.addEventListener.lastCall.args[1];
assert.calledWith(fakeFrameWindow.removeEventListener, 'message', handler);
assert.calledWith(
fakeFrameWindow.removeEventListener,
'message',
handler
);
});
it('allows `startDiscovery` to be called with a new handler', () => {
......
......@@ -12,15 +12,20 @@
*/
function assertPromiseIsRejected(promise, expectedErr) {
const rejectFlag = {};
return promise.catch(err => {
assert.equal(err.message, expectedErr);
return rejectFlag;
}).then(result => {
assert.equal(result, rejectFlag, 'expected promise to be rejected but it was fulfilled');
});
return promise
.catch(err => {
assert.equal(err.message, expectedErr);
return rejectFlag;
})
.then(result => {
assert.equal(
result,
rejectFlag,
'expected promise to be rejected but it was fulfilled'
);
});
}
/**
* Takes a Promise<T> and returns a Promise<Result>
* where Result = { result: T } | { error: any }.
......@@ -31,11 +36,13 @@ function assertPromiseIsRejected(promise, expectedErr) {
* Consider using `assertPromiseIsRejected` instead.
*/
function toResult(promise) {
return promise.then(function (result) {
return { result: result };
}).catch(function (err) {
return { error: err };
});
return promise
.then(function(result) {
return { result: result };
})
.catch(function(err) {
return { error: err };
});
}
module.exports = {
......
......@@ -4,8 +4,7 @@ const settings = require('../settings');
const sandbox = sinon.sandbox.create();
describe('settings', function () {
describe('settings', function() {
afterEach('reset the sandbox', function() {
sandbox.restore();
});
......@@ -24,7 +23,7 @@ describe('settings', function () {
afterEach('remove js-hypothesis-config tags', function() {
const elements = document.querySelectorAll('.js-settings-test');
for (let i=0; i < elements.length; i++) {
for (let i = 0; i < elements.length; i++) {
elements[i].remove();
}
});
......@@ -53,10 +52,7 @@ describe('settings', function () {
});
it('returns the array, parsed into an object', function() {
assert.deepEqual(
jsonConfigsFrom(document),
{0: 'a', 1: 'b', 2: 'c'}
);
assert.deepEqual(jsonConfigsFrom(document), { 0: 'a', 1: 'b', 2: 'c' });
});
});
......@@ -66,7 +62,7 @@ describe('settings', function () {
});
it('returns the string, parsed into an object', function() {
assert.deepEqual(jsonConfigsFrom(document), {0: 'h', 1: 'i'});
assert.deepEqual(jsonConfigsFrom(document), { 0: 'h', 1: 'i' });
});
});
......@@ -92,7 +88,7 @@ describe('settings', function () {
it('still returns settings from other JSON scripts', function() {
appendJSHypothesisConfig(document, '{"foo": "FOO", "bar": "BAR"}');
assert.deepEqual(jsonConfigsFrom(document), {foo: 'FOO', bar: 'BAR'});
assert.deepEqual(jsonConfigsFrom(document), { foo: 'FOO', bar: 'BAR' });
});
});
......@@ -112,10 +108,7 @@ describe('settings', function () {
});
it('returns the settings', function() {
assert.deepEqual(
jsonConfigsFrom(document),
{foo: 'FOO', bar: 'BAR'}
);
assert.deepEqual(jsonConfigsFrom(document), { foo: 'FOO', bar: 'BAR' });
});
});
......@@ -127,10 +120,11 @@ describe('settings', function () {
});
it('merges them all into one returned object', function() {
assert.deepEqual(
jsonConfigsFrom(document),
{foo: 'FOO', bar: 'BAR', gar: 'GAR'}
);
assert.deepEqual(jsonConfigsFrom(document), {
foo: 'FOO',
bar: 'BAR',
gar: 'GAR',
});
});
});
......@@ -141,9 +135,12 @@ describe('settings', function () {
appendJSHypothesisConfig(document, '{"foo": "third"}');
});
specify('settings from later in the page override ones from earlier', function() {
assert.equal(jsonConfigsFrom(document).foo, 'third');
});
specify(
'settings from later in the page override ones from earlier',
function() {
assert.equal(jsonConfigsFrom(document).foo, 'third');
}
);
});
});
});
......@@ -19,7 +19,7 @@
function noCallThru(stubs) {
// This function is trivial but serves as documentation for why
// '@noCallThru' is used.
return Object.assign(stubs, {'@noCallThru':true});
return Object.assign(stubs, { '@noCallThru': true });
}
/**
......@@ -52,18 +52,20 @@ function noCallThru(stubs) {
* @param {Array<T>} fixtures - Array of fixture objects.
*/
function unroll(description, testFn, fixtures) {
fixtures.forEach(function (fixture) {
const caseDescription = Object.keys(fixture).reduce(function (desc, key) {
fixtures.forEach(function(fixture) {
const caseDescription = Object.keys(fixture).reduce(function(desc, key) {
return desc.replace('#' + key, String(fixture[key]));
}, description);
it(caseDescription, function (done) {
it(caseDescription, function(done) {
if (testFn.length === 1) {
// Test case does not accept a 'done' callback argument, so we either
// call done() immediately if it returns a non-Promiselike object
// or when the Promise resolves otherwise
const result = testFn(fixture);
if (typeof result === 'object' && result.then) {
result.then(function () { done(); }, done);
result.then(function() {
done();
}, done);
} else {
done();
}
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
......@@ -5,7 +5,7 @@ function DropdownMenuBtnController($timeout) {
const self = this;
this.toggleDropdown = function($event) {
$event.stopPropagation();
$timeout(function () {
$timeout(function() {
self.onToggleDropdown();
}, 0);
};
......
This diff is collapsed.
This diff is collapsed.
......@@ -3,7 +3,7 @@
module.exports = {
controllerAs: 'vm',
template: require('../templates/help-link.html'),
controller: function () {},
controller: function() {},
scope: {
version: '<',
userAgent: '<',
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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