Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tests and other tweaks to shared/settings.js #406

Merged
merged 4 commits into from
May 31, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/annotator/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ function configFrom(window_) {

// Parse config from `<script class="js-hypothesis-config">` tags
try {
Object.assign(config, settings(window_.document));
Object.assign(config, settings.jsonConfigsFrom(window_.document));
} catch (err) {
console.warn('Could not parse settings from js-hypothesis-config tags',
err);
Expand Down
35 changes: 20 additions & 15 deletions src/annotator/test/config-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@

var proxyquire = require('proxyquire');

var fakeSettings = sinon.stub();
var fakeSettings = {
jsonConfigsFrom: sinon.stub(),
};
var fakeExtractAnnotationQuery = {};

var configFrom = proxyquire('../config', {
Expand All @@ -26,8 +28,8 @@ describe('annotator.config', function() {
});

beforeEach('reset fakeSettings', function() {
fakeSettings.reset();
fakeSettings.returns({});
fakeSettings.jsonConfigsFrom.reset();
fakeSettings.jsonConfigsFrom.returns({});
});

beforeEach('reset fakeExtractAnnotationQuery', function() {
Expand Down Expand Up @@ -71,25 +73,26 @@ describe('annotator.config', function() {

configFrom(window_);

assert.calledOnce(fakeSettings);
assert.calledWithExactly(fakeSettings, window_.document);
assert.calledOnce(fakeSettings.jsonConfigsFrom);
assert.calledWithExactly(
fakeSettings.jsonConfigsFrom, window_.document);
});

context('when settings() returns a non-empty object', function() {
context('when jsonConfigsFrom() returns a non-empty object', function() {
it('reads the setting into the returned config', function() {
// configFrom() just blindly adds any key: value settings that settings()
// returns into the returned config object.
fakeSettings.returns({foo: 'bar'});
// configFrom() just blindly adds any key: value settings that
// jsonConfigsFrom() returns into the returns options object.
fakeSettings.jsonConfigsFrom.returns({foo: 'bar'});

var config = configFrom(fakeWindow());

assert.equal(config.foo, 'bar');
});
});

context('when settings() throws an error', function() {
context('when jsonConfigsFrom() throws an error', function() {
beforeEach(function() {
fakeSettings.throws();
fakeSettings.jsonConfigsFrom.throws();
});

it('catches the error', function() {
Expand Down Expand Up @@ -117,7 +120,9 @@ describe('annotator.config', function() {
var window_ = fakeWindow();
window_.hypothesisConfig = sinon.stub().returns({
foo: 'fooFromHypothesisConfigFunc'});
fakeSettings.returns({foo: 'fooFromJSHypothesisConfigObj'});
fakeSettings.jsonConfigsFrom.returns({
foo: 'fooFromJSHypothesisConfigObj',
});

var config = configFrom(window_);

Expand Down Expand Up @@ -170,7 +175,7 @@ describe('annotator.config', function() {
},
].forEach(function(test) {
it(test.name, function() {
fakeSettings.returns({showHighlights: test.in});
fakeSettings.jsonConfigsFrom.returns({showHighlights: test.in});

var config = configFrom(fakeWindow());

Expand Down Expand Up @@ -200,12 +205,12 @@ describe('annotator.config', function() {

specify('settings from extractAnnotationQuery override others', function() {
// Settings returned by extractAnnotationQuery() override ones from
// settings() or from window.hypothesisConfig().
// jsonConfigsFrom() or from window.hypothesisConfig().
var window_ = fakeWindow();
fakeExtractAnnotationQuery.extractAnnotationQuery.returns({
foo: 'fromExtractAnnotationQuery',
});
fakeSettings.returns({foo: 'fromSettings'});
fakeSettings.jsonConfigsFrom.returns({foo: 'fromSettings'});
window_.hypothesisConfig = sinon.stub().returns({
foo: 'fromHypothesisConfig',
});
Expand Down
2 changes: 1 addition & 1 deletion src/boot/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
/* global __MANIFEST__ */

var boot = require('./boot');
var settings = require('../shared/settings')(document);
var settings = require('../shared/settings').jsonConfigsFrom(document);

boot(document, {
assetRoot: settings.assetRoot || '__ASSET_ROOT__',
Expand Down
23 changes: 15 additions & 8 deletions src/shared/settings.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@ function assign(dest, src) {
}

/**
* Return application configuration information from the host page.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was actually wrong - the function isn't always used to read settings from the host page

* Return a parsed `js-hypothesis-config` object from the document, or `{}`.
*
* Exposes shared application settings, read from script tags with the
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also misleading. It's a shared utility function, yes. But "shared application settings" is misleading. Not every setting read from a js-hypothesis-config is actually shared between the three legs of the app (annotator, boot and sidebar), nor is every setting that is shared read from this function.

* class `js-hypothesis-config` which contain JSON content.
* Find all `<script class="js-hypothesis-config">` tags in the given document,
* parse them as JSON, and return the parsed object.
*
* If there are multiple such tags, the configuration from each is merged.
* If there are no `js-hypothesis-config` tags in the document then return
* `{}`.
*
* @param {Document|Element} document - The root element to search for
* <script> settings tags.
* If there are multiple `js-hypothesis-config` tags in the document then merge
* them into a single returned object (when multiple scripts contain the same
* setting names, scripts further down in the document override those further
* up).
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've left the exception that this function throws (on invalid JSON) still undocumented, as I intend to send a PR to stop iut from throwing the exception anyway

*
* @param {Document|Element} document - The root element to search.
*/
function settings(document) {
function jsonConfigsFrom(document) {
var settingsElements =
document.querySelectorAll('script.js-hypothesis-config');

Expand All @@ -33,4 +38,6 @@ function settings(document) {
return config;
}

module.exports = settings;
module.exports = {
jsonConfigsFrom: jsonConfigsFrom,
};
145 changes: 120 additions & 25 deletions src/shared/test/settings-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,128 @@

var settings = require('../settings');

function createConfigElement(obj) {
var el = document.createElement('script');
el.type = 'application/json';
el.textContent = JSON.stringify(obj);
el.classList.add('js-hypothesis-config');
el.classList.add('js-settings-test');
return el;
}

function removeJSONScriptTags() {
var elements = document.querySelectorAll('.js-settings-test');
for (var i=0; i < elements.length; i++) {
elements[i].parentNode.removeChild(elements[i]);
}
}

describe('settings', function () {
afterEach(removeJSONScriptTags);
describe('#jsonConfigsFrom', function() {
var jsonConfigsFrom = settings.jsonConfigsFrom;

it('reads config from .js-hypothesis-config <script> tags', function () {
document.body.appendChild(createConfigElement({key:'value'}));
assert.deepEqual(settings(document), {key:'value'});
});
function appendJSHypothesisConfig(document_, jsonString) {
var el = document_.createElement('script');
el.type = 'application/json';
el.textContent = jsonString;
el.classList.add('js-hypothesis-config');
el.classList.add('js-settings-test');
document_.body.appendChild(el);
}

afterEach('remove js-hypothesis-config tags', function() {
var elements = document.querySelectorAll('.js-settings-test');
for (var i=0; i < elements.length; i++) {
elements[i].remove();
}
});

context('when there are no JSON scripts', function() {
it('returns {}', function() {
assert.deepEqual(jsonConfigsFrom(document), {});
});
});

context("when there's JSON scripts with no top-level objects", function() {
beforeEach('add JSON scripts with no top-level objects', function() {
appendJSHypothesisConfig(document, 'null');
appendJSHypothesisConfig(document, '23');
appendJSHypothesisConfig(document, 'true');
});

it('ignores them', function() {
assert.deepEqual(jsonConfigsFrom(document), {});
});
});

context("when there's a JSON script with a top-level array", function() {
beforeEach('add a JSON script containing a top-level array', function() {
appendJSHypothesisConfig(document, '["a", "b", "c"]');
});

it('returns the array, parsed into an object', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix this in a separate pr, it should ignore top-level arrays (but log a warning)

assert.deepEqual(
jsonConfigsFrom(document),
{0: 'a', 1: 'b', 2: 'c'}
);
});
});

context("when there's a JSON script with a top-level string", function() {
beforeEach('add a JSON script with a top-level string', function() {
appendJSHypothesisConfig(document, '"hi"');
});

it('returns the string, parsed into an object', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix this in a separate pr, it should ignore top-level strings (but log a warning)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heh. LOLJS :)

assert.deepEqual(jsonConfigsFrom(document), {0: 'h', 1: 'i'});
});
});

context("when there's a JSON script containing invalid JSON", function() {
beforeEach('add a JSON script containing invalid JSON', function() {
appendJSHypothesisConfig(document, 'this is not valid json');
});

it('throws a SyntaxError', function() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should fix this in a follow up pr: it should log a warning, and ignore the invalid json, not throw

assert.throws(
function() { jsonConfigsFrom(document); },
SyntaxError
);
});
});

context("when there's a JSON script with an empty object", function() {
beforeEach('add a JSON script containing an empty object', function() {
appendJSHypothesisConfig(document, '{}');
});

it('ignores it', function() {
assert.deepEqual(jsonConfigsFrom(document), {});
});
});

context("when there's a JSON script containing some settings", function() {
beforeEach('add a JSON script containing some settings', function() {
appendJSHypothesisConfig(document, '{"foo": "FOO", "bar": "BAR"}');
});

it('returns the settings', function() {
assert.deepEqual(
jsonConfigsFrom(document),
{foo: 'FOO', bar: 'BAR'}
);
});
});

context('when there are JSON scripts with different settings', function() {
beforeEach('add some JSON scripts with different settings', function() {
appendJSHypothesisConfig(document, '{"foo": "FOO"}');
appendJSHypothesisConfig(document, '{"bar": "BAR"}');
appendJSHypothesisConfig(document, '{"gar": "GAR"}');
});

it('merges them all into one returned object', function() {
assert.deepEqual(
jsonConfigsFrom(document),
{foo: 'FOO', bar: 'BAR', gar: 'GAR'}
);
});
});

context('when multiple JSON scripts contain the same setting', function() {
beforeEach('add some JSON scripts with different settings', function() {
appendJSHypothesisConfig(document, '{"foo": "first"}');
appendJSHypothesisConfig(document, '{"foo": "second"}');
appendJSHypothesisConfig(document, '{"foo": "third"}');
});

it('merges settings from all config <script> tags', function () {
document.body.appendChild(createConfigElement({a: 1}));
document.body.appendChild(createConfigElement({b: 2}));
assert.deepEqual(settings(document), {a: 1, b: 2});
specify('settings from later in the page override ones from earlier', function() {
assert.equal(jsonConfigsFrom(document).foo, 'third');
});
});
});
});
2 changes: 1 addition & 1 deletion src/sidebar/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ require('../shared/polyfills');
var raven;

// Read settings rendered into sidebar app HTML by service/extension.
var settings = require('../shared/settings')(document);
var settings = require('../shared/settings').jsonConfigsFrom(document);

if (settings.raven) {
// Initialize Raven. This is required at the top of this file
Expand Down