Commit 11dde525 authored by Robert Knight's avatar Robert Knight

Always use main URL from main frame when searching for groups

Previously the groups service used the first HTTP(S) URL returned by
`store.searchUris()` to get the URL used to search for associated groups. In
practice, the URIs from the main frame always appeared first in this
list, and the main URL from that frame was the first entry within that sublist.
However, the `store.searchUris` function doesn't provide any guarantee of this
and future changes could break this invariant.

This commit adds a `store.mainFrame()` function which returns the main
frame, the one with no `id`, and modifies the groups service to use the
main URI for that frame.

A change in behavior here is that if the top-level frame was a non-HTTPS
frame but embedded an HTTPS frame, then the group search would have used
the URL from the HTTPS frame. Now it will use the non-HTTPS URL from the
top-level frame.
parent 64adde7b
...@@ -37,15 +37,11 @@ function groups( ...@@ -37,15 +37,11 @@ function groups(
function getDocumentUriForGroupSearch() { function getDocumentUriForGroupSearch() {
function mainUri() { function mainUri() {
const uris = store.searchUris(); const mainFrame = store.mainFrame();
if (uris.length === 0) { if (!mainFrame) {
return null; return null;
} }
return mainFrame.uri;
// We get the first HTTP URL here on the assumption that group scopes must
// be domains (+paths)? and therefore we need to look up groups based on
// HTTP URLs (so eg. we cannot use a "file:" URL or PDF fingerprint).
return uris.find(uri => uri.startsWith('http'));
} }
return awaitStateChange(store, mainUri); return awaitStateChange(store, mainUri);
} }
......
...@@ -61,7 +61,7 @@ describe('groups', function() { ...@@ -61,7 +61,7 @@ describe('groups', function() {
fakeStore = fakeReduxStore( fakeStore = fakeReduxStore(
{ {
searchUris: ['http://example.org'], mainFrame: { uri: 'http://example.org' },
focusedGroup: null, focusedGroup: null,
groups: [], groups: [],
directLinkedGroupId: null, directLinkedGroupId: null,
...@@ -80,8 +80,8 @@ describe('groups', function() { ...@@ -80,8 +80,8 @@ describe('groups', function() {
focusedGroup() { focusedGroup() {
return this.getState().focusedGroup; return this.getState().focusedGroup;
}, },
searchUris() { mainFrame() {
return this.getState().searchUris; return this.getState().mainFrame;
}, },
focusedGroupId() { focusedGroupId() {
const group = this.getState().focusedGroup; const group = this.getState().focusedGroup;
...@@ -417,9 +417,9 @@ describe('groups', function() { ...@@ -417,9 +417,9 @@ describe('groups', function() {
it('waits for the document URL to be determined', () => { it('waits for the document URL to be determined', () => {
const svc = service(); const svc = service();
fakeStore.setState({ searchUris: [] }); fakeStore.setState({ mainFrame: null });
const loaded = svc.load(); const loaded = svc.load();
fakeStore.setState({ searchUris: ['https://asite.com'] }); fakeStore.setState({ mainFrame: { uri: 'https://asite.com' } });
return loaded.then(() => { return loaded.then(() => {
assert.calledWith(fakeApi.groups.list, { assert.calledWith(fakeApi.groups.list, {
...@@ -436,7 +436,7 @@ describe('groups', function() { ...@@ -436,7 +436,7 @@ describe('groups', function() {
}); });
it('does not wait for the document URL', () => { it('does not wait for the document URL', () => {
fakeStore.setState({ searchUris: [] }); fakeStore.setState({ mainFrame: null });
const svc = service(); const svc = service();
return svc.load().then(() => { return svc.load().then(() => {
assert.calledWith(fakeApi.groups.list, { assert.calledWith(fakeApi.groups.list, {
...@@ -804,14 +804,16 @@ describe('groups', function() { ...@@ -804,14 +804,16 @@ describe('groups', function() {
it('should refetch groups if main frame URL has changed', () => { it('should refetch groups if main frame URL has changed', () => {
const svc = service(); const svc = service();
fakeStore.setState({ searchUris: ['https://domain.com/page-a'] }); fakeStore.setState({ mainFrame: { uri: 'https://domain.com/page-a' } });
return svc return svc
.load() .load()
.then(() => { .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();
fakeStore.setState({ searchUris: ['https://domain.com/page-b'] }); fakeStore.setState({
mainFrame: { uri: 'https://domain.com/page-b' },
});
return fakeRootScope.eventCallbacks[events.FRAME_CONNECTED](); return fakeRootScope.eventCallbacks[events.FRAME_CONNECTED]();
}) })
...@@ -823,7 +825,7 @@ describe('groups', function() { ...@@ -823,7 +825,7 @@ describe('groups', function() {
it('should not refetch groups if main frame URL has not changed', () => { it('should not refetch groups if main frame URL has not changed', () => {
const svc = service(); const svc = service();
fakeStore.setState({ searchUris: ['https://domain.com/page-a'] }); fakeStore.setState({ mainFrame: { uri: 'https://domain.com/page-a' } });
return svc return svc
.load() .load()
.then(() => { .then(() => {
......
'use strict'; 'use strict';
const { createSelector } = require('reselect');
const util = require('../util'); const util = require('../util');
function init() { function init() {
...@@ -71,6 +73,24 @@ function frames(state) { ...@@ -71,6 +73,24 @@ function frames(state) {
return state.frames; return state.frames;
} }
/**
* Return the "main" frame that the sidebar is connected to.
*
* The sidebar may be connected to multiple frames from different URLs.
* For some purposes, the main frame (typically the top-level one that contains
* the sidebar) needs to be distinguished. This selector returns the main frame
* for that purpose.
*
* This may be `null` during startup.
*/
const mainFrame = createSelector(
state => state.frames,
// Sub-frames will all have a "frame identifier" set. The main frame is the
// one with a `null` id.
frames => frames.find(f => !f.id) || null
);
function searchUrisForFrame(frame) { function searchUrisForFrame(frame) {
let uris = [frame.uri]; let uris = [frame.uri];
...@@ -113,6 +133,7 @@ module.exports = { ...@@ -113,6 +133,7 @@ module.exports = {
selectors: { selectors: {
frames, frames,
mainFrame,
searchUris, searchUris,
}, },
}; };
'use strict'; 'use strict';
const frames = require('../frames'); const frames = require('../frames');
const createStore = require('../../create-store');
const session = require('../session'); const session = require('../session');
const util = require('../../util'); const util = require('../../util');
const unroll = require('../../../../shared/test/util').unroll; const unroll = require('../../../../shared/test/util').unroll;
...@@ -13,7 +15,16 @@ function init() { ...@@ -13,7 +15,16 @@ function init() {
return Object.assign({}, frames.init(), session.init()); return Object.assign({}, frames.init(), session.init());
} }
describe('frames reducer', function() { describe('sidebar/store/modules/frames', function() {
let store;
beforeEach(() => {
// Setup a store for tests. Note that some of the tests in this module
// pre-date the `createStore` helper and have not been refactored to use
// it yet.
store = createStore([frames]);
});
describe('#connectFrame', function() { describe('#connectFrame', function() {
it('adds the frame to the list of connected frames', function() { it('adds the frame to the list of connected frames', function() {
const frame = { uri: 'http://example.com' }; const frame = { uri: 'http://example.com' };
...@@ -71,6 +82,34 @@ describe('frames reducer', function() { ...@@ -71,6 +82,34 @@ describe('frames reducer', function() {
}); });
}); });
describe('#mainFrame', () => {
it('returns `null` if no frames are connected', () => {
assert.isNull(store.mainFrame());
});
[
{
frames: [{ id: null, uri: 'https://example.org' }],
expectedFrame: 0,
},
{
frames: [
// An iframe which is also connected.
{ id: 'iframe1', uri: 'https://foo.com/' },
// The top-level frame.
{ id: null, uri: 'https://example.org' },
],
expectedFrame: 1,
},
].forEach(({ frames, expectedFrame }) => {
it('returns the main frame from the frames connected to the sidebar', () => {
frames.forEach(frame => store.connectFrame(frame));
assert.equal(store.mainFrame(), frames[expectedFrame]);
});
});
});
describe('#searchUris', function() { describe('#searchUris', function() {
unroll( unroll(
'returns the expected search URIs (#when)', 'returns the expected search URIs (#when)',
......
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