Unverified Commit 59e8b4f6 authored by Robert Knight's avatar Robert Knight Committed by GitHub

Merge pull request #2038 from hypothesis/remove-frame-connected

Remove `FRAME_CONNECTED` event
parents 389d6aa4 2a6f50bd
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
* on $rootScope * on $rootScope
*/ */
export default { export default {
// Internal state changes
FRAME_CONNECTED: 'frameConnected',
// Session state changes // Session state changes
/** The logged-in user changed */ /** The logged-in user changed */
......
...@@ -12,23 +12,20 @@ ...@@ -12,23 +12,20 @@
import bridgeEvents from '../../shared/bridge-events'; import bridgeEvents from '../../shared/bridge-events';
import warnOnce from '../../shared/warn-once'; import warnOnce from '../../shared/warn-once';
import events from '../events'; import { watch } from '../util/watch';
// @ngInject // @ngInject
export default function features($rootScope, bridge, session) { export default function features(bridge, session, store) {
const _sendFeatureFlags = function () { const currentFlags = () => store.profile().features;
const userFeatures = session.state.features; const sendFeatureFlags = () => {
bridge.call(bridgeEvents.FEATURE_FLAGS_UPDATED, userFeatures || {}); bridge.call(bridgeEvents.FEATURE_FLAGS_UPDATED, currentFlags() || {});
}; };
// user changed is currently called when we initially load // Re-send feature flags to connected frames when flags change or a new
// the sidebar and when the user actually logs out/in. // frame connects.
$rootScope.$on(events.USER_CHANGED, _sendFeatureFlags); watch(store.subscribe, [currentFlags, () => store.frames()], () =>
sendFeatureFlags()
// send on frame connected as well because the user_changed event );
// alone might run before the frames ever connected. This will
// provide us the follow up to make sure that the frames get the flags
$rootScope.$on(events.FRAME_CONNECTED, _sendFeatureFlags);
/** /**
* Returns true if the flag with the given name is enabled for the current * Returns true if the flag with the given name is enabled for the current
...@@ -43,17 +40,18 @@ export default function features($rootScope, bridge, session) { ...@@ -43,17 +40,18 @@ export default function features($rootScope, bridge, session) {
// (see CACHE_TTL in session.js) // (see CACHE_TTL in session.js)
session.load(); session.load();
if (!session.state.features) { const flags = currentFlags();
if (!flags) {
// features data has not yet been fetched // features data has not yet been fetched
return false; return false;
} }
const features = session.state.features; if (!(flag in flags)) {
if (!(flag in features)) {
warnOnce('looked up unknown feature', flag); warnOnce('looked up unknown feature', flag);
return false; return false;
} }
return features[flag]; return flags[flag];
} }
return { return {
......
...@@ -203,7 +203,6 @@ export default function FrameSync($rootScope, $window, store, bridge) { ...@@ -203,7 +203,6 @@ export default function FrameSync($rootScope, $window, store, bridge) {
return; return;
} }
$rootScope.$broadcast(events.FRAME_CONNECTED);
store.connectFrame({ store.connectFrame({
id: info.frameIdentifier, id: info.frameIdentifier,
metadata: info.metadata, metadata: info.metadata,
......
...@@ -3,6 +3,7 @@ import serviceConfig from '../service-config'; ...@@ -3,6 +3,7 @@ import serviceConfig from '../service-config';
import { isReply } from '../util/annotation-metadata'; import { isReply } from '../util/annotation-metadata';
import { combineGroups } from '../util/groups'; import { combineGroups } from '../util/groups';
import { awaitStateChange } from '../util/state'; import { awaitStateChange } from '../util/state';
import { watch } from '../util/watch';
const DEFAULT_ORG_ID = '__default__'; const DEFAULT_ORG_ID = '__default__';
...@@ -31,7 +32,10 @@ export default function groups( ...@@ -31,7 +32,10 @@ export default function groups(
const svc = serviceConfig(settings); const svc = serviceConfig(settings);
const authority = svc ? svc.authority : null; const authority = svc ? svc.authority : null;
function getDocumentUriForGroupSearch() { /**
* Return the main document URI that is used to fetch groups associated with
* the site that the user is on.
*/
function mainUri() { function mainUri() {
const mainFrame = store.mainFrame(); const mainFrame = store.mainFrame();
if (!mainFrame) { if (!mainFrame) {
...@@ -39,6 +43,8 @@ export default function groups( ...@@ -39,6 +43,8 @@ export default function groups(
} }
return mainFrame.uri; return mainFrame.uri;
} }
function getDocumentUriForGroupSearch() {
return awaitStateChange(store, mainUri); return awaitStateChange(store, mainUri);
} }
...@@ -167,11 +173,7 @@ export default function groups( ...@@ -167,11 +173,7 @@ export default function groups(
}); });
} }
// The document URI passed to the most recent `GET /api/groups` call in order let didSubscribeToUriChanges = false;
// to include groups associated with this page. This is retained to determine
// whether we need to re-fetch groups if the URLs of frames connected to the
// sidebar app changes.
let documentUri = null;
/* /*
* Fetch an individual group. * Fetch an individual group.
...@@ -193,15 +195,27 @@ export default function groups( ...@@ -193,15 +195,27 @@ export default function groups(
* The groups that are fetched depend on the current user, the URI of * The groups that are fetched depend on the current user, the URI of
* the current document, and the direct-linked group and/or annotation. * the current document, and the direct-linked group and/or annotation.
* *
* On startup, `load()` must be called to trigger the initial groups fetch.
* Subsequently groups are automatically reloaded if the logged-in user or
* main document URI changes.
*
* @return {Promise<Group[]>} * @return {Promise<Group[]>}
*/ */
async function load() { async function load() {
// Step 1: Get the URI of the active document, so we can fetch groups // Step 1: Get the URI of the active document, so we can fetch groups
// associated with that document. // associated with that document.
let documentUri;
if (isSidebar) { if (isSidebar) {
documentUri = await getDocumentUriForGroupSearch(); documentUri = await getDocumentUriForGroupSearch();
} }
if (!didSubscribeToUriChanges) {
didSubscribeToUriChanges = true;
watch(store.subscribe, mainUri, () => {
load();
});
}
// Step 2: Concurrently fetch the groups the user is a member of, // Step 2: Concurrently fetch the groups the user is a member of,
// the groups associated with the current document and the annotation // the groups associated with the current document and the annotation
// and/or group that was direct-linked (if any). // and/or group that was direct-linked (if any).
...@@ -362,16 +376,6 @@ export default function groups( ...@@ -362,16 +376,6 @@ export default function groups(
return load(); return load();
}); });
// refetch the list of groups when document url changes
$rootScope.$on(events.FRAME_CONNECTED, () => {
return getDocumentUriForGroupSearch().then(uri => {
if (documentUri !== uri) {
documentUri = uri;
load();
}
});
});
return { return {
all: all, all: all,
get: get, get: get,
......
import bridgeEvents from '../../../shared/bridge-events'; import bridgeEvents from '../../../shared/bridge-events';
import events from '../../events';
import features from '../features'; import features from '../features';
import { $imports } from '../features'; import { $imports } from '../features';
describe('h:features - sidebar layer', function () { describe('sidebar/services/features', function () {
let fakeBridge; let fakeBridge;
let fakeWarnOnce; let fakeWarnOnce;
let fakeRootScope;
let fakeSession; let fakeSession;
let sandbox; let fakeStore;
beforeEach(function () { beforeEach(function () {
sandbox = sinon.createSandbox();
fakeBridge = { fakeBridge = {
call: sinon.stub(), call: sinon.stub(),
}; };
fakeWarnOnce = sinon.stub(); fakeWarnOnce = sinon.stub();
fakeRootScope = {
eventCallbacks: {},
$broadcast: sandbox.stub(),
$on: function (event, callback) {
this.eventCallbacks[event] = callback;
},
};
fakeSession = { fakeSession = {
load: sinon.stub(), load: sinon.stub(),
state: { };
fakeStore = {
subscribe: sinon.stub(),
frames: sinon.stub().returns([]),
profile: sinon.stub().returns({
features: { features: {
feature_on: true, feature_on: true,
feature_off: false, feature_off: false,
}, },
}, }),
}; };
$imports.$mock({ $imports.$mock({
...@@ -46,64 +37,83 @@ describe('h:features - sidebar layer', function () { ...@@ -46,64 +37,83 @@ describe('h:features - sidebar layer', function () {
afterEach(function () { afterEach(function () {
$imports.$restore(); $imports.$restore();
sandbox.restore();
}); });
function createService() {
return features(fakeBridge, fakeSession, fakeStore);
}
describe('flagEnabled', function () { describe('flagEnabled', function () {
it('should retrieve features data', function () { it('should retrieve features data', function () {
const features_ = features(fakeRootScope, fakeBridge, fakeSession); const features_ = createService();
assert.equal(features_.flagEnabled('feature_on'), true); assert.equal(features_.flagEnabled('feature_on'), true);
assert.equal(features_.flagEnabled('feature_off'), false); 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 () {
const features_ = features(fakeRootScope, fakeBridge, fakeSession); const features_ = createService();
// simulate feature data not having been loaded yet // Simulate feature data not having been loaded yet
fakeSession.state = {}; fakeStore.profile.returns({});
assert.equal(features_.flagEnabled('feature_on'), false); assert.equal(features_.flagEnabled('feature_on'), false);
}); });
it('should trigger a refresh of session data', function () { it('should trigger a refresh of session data', function () {
const features_ = features(fakeRootScope, fakeBridge, fakeSession); const features_ = createService();
features_.flagEnabled('feature_on'); features_.flagEnabled('feature_on');
assert.calledOnce(fakeSession.load); assert.calledOnce(fakeSession.load);
}); });
it('should return false for unknown flags', function () { it('should return false for unknown flags', function () {
const features_ = features(fakeRootScope, fakeBridge, fakeSession); const features_ = createService();
assert.isFalse(features_.flagEnabled('unknown_feature')); assert.isFalse(features_.flagEnabled('unknown_feature'));
}); });
}); });
it('should broadcast feature flags to annotation layer based on load/user changes', function () { function notifyStoreSubscribers() {
assert.notProperty(fakeRootScope.eventCallbacks, events.USER_CHANGED); const subscribers = fakeStore.subscribe.args.map(args => args[0]);
assert.notProperty(fakeRootScope.eventCallbacks, events.FRAME_CONNECTED); subscribers.forEach(s => s());
}
features(fakeRootScope, fakeBridge, fakeSession);
assert.property(fakeRootScope.eventCallbacks, events.USER_CHANGED); it('should broadcast feature flags to annotator if flags change', () => {
assert.property(fakeRootScope.eventCallbacks, events.FRAME_CONNECTED); createService();
// respond to user changing by broadcasting the feature flags // First update, with no changes to feature flags.
notifyStoreSubscribers();
assert.notCalled(fakeBridge.call); assert.notCalled(fakeBridge.call);
fakeRootScope.eventCallbacks[events.USER_CHANGED](); // Second update, with changes to feature flags.
fakeStore.profile.returns({
features: {
feature_on: true,
feature_off: true,
},
});
notifyStoreSubscribers();
assert.calledOnce(fakeBridge.call);
assert.calledWith( assert.calledWith(
fakeBridge.call, fakeBridge.call,
bridgeEvents.FEATURE_FLAGS_UPDATED, bridgeEvents.FEATURE_FLAGS_UPDATED,
fakeSession.state.features fakeStore.profile().features
); );
});
it('should broadcast feature flags to annotator if a new frame connects', () => {
createService();
// First update, with no changes to frames.
notifyStoreSubscribers();
assert.notCalled(fakeBridge.call);
// Second update, with changes to frames.
fakeStore.frames.returns([{ uri: 'https://example.com' }]);
// respond to frame connections by broadcasting the feature flags notifyStoreSubscribers();
fakeRootScope.eventCallbacks[events.FRAME_CONNECTED]();
assert.calledTwice(fakeBridge.call);
assert.calledWith( assert.calledWith(
fakeBridge.call, fakeBridge.call,
bridgeEvents.FEATURE_FLAGS_UPDATED, bridgeEvents.FEATURE_FLAGS_UPDATED,
fakeSession.state.features fakeStore.profile().features
); );
}); });
}); });
import events from '../../events'; import events from '../../events';
import fakeReduxStore from '../../test/fake-redux-store'; import fakeReduxStore from '../../test/fake-redux-store';
import groups, { $imports } from '../groups'; import groups, { $imports } from '../groups';
import { waitFor } from '../../../test-util/wait';
/** /**
* Generate a truth table containing every possible combination of a set of * Generate a truth table containing every possible combination of a set of
...@@ -108,7 +109,7 @@ describe('groups', function () { ...@@ -108,7 +109,7 @@ describe('groups', function () {
}, },
$on: function (event, callback) { $on: function (event, callback) {
if (event === events.USER_CHANGED || event === events.FRAME_CONNECTED) { if (event === events.USER_CHANGED) {
this.eventCallbacks[event] = callback; this.eventCallbacks[event] = callback;
} }
}, },
...@@ -809,7 +810,7 @@ describe('groups', function () { ...@@ -809,7 +810,7 @@ describe('groups', function () {
}); });
}); });
describe('calls load on various events', function () { describe('automatic re-fetching', function () {
it('refetches groups when the logged-in user changes', () => { it('refetches groups when the logged-in user changes', () => {
service(); service();
...@@ -819,15 +820,14 @@ describe('groups', function () { ...@@ -819,15 +820,14 @@ describe('groups', function () {
}); });
context('when a new frame connects', () => { context('when a new frame connects', () => {
it('should refetch groups if main frame URL has changed', () => { it('should refetch groups if main frame URL has changed', async () => {
const svc = service(); const svc = service();
fakeStore.setState({ fakeStore.setState({
frames: [{ uri: 'https://domain.com/page-a' }], frames: [{ uri: 'https://domain.com/page-a' }],
}); });
return svc await svc.load();
.load()
.then(() => {
// Simulate main frame URL change, eg. due to client-side navigation in // Simulate main frame URL change, eg. due to client-side navigation in
// a single page application. // a single page application.
fakeApi.groups.list.resetHistory(); fakeApi.groups.list.resetHistory();
...@@ -835,27 +835,31 @@ describe('groups', function () { ...@@ -835,27 +835,31 @@ describe('groups', function () {
frames: [{ uri: 'https://domain.com/page-b' }], frames: [{ uri: 'https://domain.com/page-b' }],
}); });
return fakeRootScope.eventCallbacks[events.FRAME_CONNECTED](); await waitFor(() => fakeApi.groups.list.callCount > 0);
})
.then(() => {
assert.calledOnce(fakeApi.groups.list); assert.calledOnce(fakeApi.groups.list);
}); });
});
it('should not refetch groups if main frame URL has not changed', () => { it('should not refetch groups if main frame URL has not changed', async () => {
const svc = service(); const svc = service();
fakeStore.setState({ fakeStore.setState({
frames: [{ uri: 'https://domain.com/page-a' }], frames: [{ uri: 'https://domain.com/page-a' }],
}); });
return svc
.load() await svc.load();
.then(() => {
return fakeRootScope.eventCallbacks[events.FRAME_CONNECTED]();
})
.then(() => {
assert.calledOnce(fakeApi.groups.list); assert.calledOnce(fakeApi.groups.list);
// A new frame connects, but the main frame URI remains the same.
fakeApi.groups.list.resetHistory();
fakeStore.setState({
frames: [
{ uri: 'https://domain.com/page-a' },
{ uri: 'https://domain.com/iframe-b' },
],
}); });
await new Promise(resolve => setTimeout(resolve, 1));
assert.notCalled(fakeApi.groups.list);
}); });
}); });
}); });
......
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