Commit 6489d7e3 authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Add preact UserMenu component

parent 4d12a798
'use strict';
const { createElement } = require('preact');
const { shallow } = require('enzyme');
const UserMenu = require('../user-menu');
const MenuItem = require('../menu-item');
describe('UserMenu', () => {
let fakeAuth;
let fakeBridge;
let fakeIsThirdPartyUser;
let fakeOnLogout;
let fakeProfileBridgeEvent;
let fakeServiceConfig;
let fakeServiceUrl;
let fakeSettings;
const createUserMenu = () => {
return shallow(
<UserMenu
auth={fakeAuth}
bridge={fakeBridge}
onLogout={fakeOnLogout}
serviceUrl={fakeServiceUrl}
settings={fakeSettings}
/>
).dive(); // Dive needed because this component uses `withServices`
};
const findMenuItem = (wrapper, labelText) => {
return wrapper
.find(MenuItem)
.filterWhere(n => n.prop('label') === labelText);
};
beforeEach(() => {
fakeAuth = {
displayName: 'Eleanor Fishtail',
status: 'logged-in',
userid: 'acct:eleanorFishtail@hypothes.is',
username: 'eleanorFishy',
};
fakeBridge = { call: sinon.stub() };
fakeIsThirdPartyUser = sinon.stub();
fakeOnLogout = sinon.stub();
fakeProfileBridgeEvent = 'profile-requested';
fakeServiceConfig = sinon.stub();
fakeServiceUrl = sinon.stub();
fakeSettings = {
authDomain: 'hypothes.is',
};
UserMenu.$imports.$mock({
'../util/account-id': {
isThirdPartyUser: fakeIsThirdPartyUser,
},
'../service-config': fakeServiceConfig,
'../../shared/bridge-events': {
PROFILE_REQUESTED: fakeProfileBridgeEvent,
},
});
});
afterEach(() => {
UserMenu.$imports.$restore();
});
describe('profile menu item', () => {
context('first-party user', () => {
beforeEach(() => {
fakeIsThirdPartyUser.returns(false);
fakeServiceUrl.returns('profile-link');
});
it('should be enabled', () => {
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.notOk(profileMenuItem.prop('isDisabled'));
});
it('should have a link (href)', () => {
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.equal(profileMenuItem.prop('href'), 'profile-link');
});
it('should have a callback', () => {
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isFunction(profileMenuItem.prop('onClick'));
});
});
context('third-party user', () => {
beforeEach(() => {
fakeIsThirdPartyUser.returns(true);
});
it('should be disabled if no service configured', () => {
fakeServiceConfig.returns(null);
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isTrue(profileMenuItem.prop('isDisabled'));
});
it('should be disabled if service feature not supported', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: false });
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isTrue(profileMenuItem.prop('isDisabled'));
});
it('should be enabled if service feature support', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: true });
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.notOk(profileMenuItem.prop('isDisabled'));
});
it('should have a callback if enabled', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: true });
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
assert.isFunction(profileMenuItem.prop('onClick'));
});
});
describe('profile-selected callback', () => {
it('should fire profile event for third-party user', () => {
fakeServiceConfig.returns({ onProfileRequestProvided: true });
fakeIsThirdPartyUser.returns(true);
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
const onProfileSelected = profileMenuItem.prop('onClick');
onProfileSelected();
assert.equal(fakeBridge.call.callCount, 1);
assert.calledWith(fakeBridge.call, fakeProfileBridgeEvent);
});
it('should not fire profile event for first-party user', () => {
fakeIsThirdPartyUser.returns(false);
const wrapper = createUserMenu();
const profileMenuItem = findMenuItem(wrapper, fakeAuth.displayName);
const onProfileSelected = profileMenuItem.prop('onClick');
onProfileSelected();
assert.equal(fakeBridge.call.callCount, 0);
});
});
});
describe('account settings menu item', () => {
it('should be present if first-party user', () => {
fakeIsThirdPartyUser.returns(false);
const wrapper = createUserMenu();
const accountMenuItem = findMenuItem(wrapper, 'Account settings');
assert.isTrue(accountMenuItem.exists());
assert.calledWith(fakeServiceUrl, 'account.settings');
});
it('should not be present if third-party user', () => {
fakeIsThirdPartyUser.returns(true);
const wrapper = createUserMenu();
const accountMenuItem = findMenuItem(wrapper, 'Account settings');
assert.isFalse(accountMenuItem.exists());
});
});
describe('log out menu item', () => {
const tests = [
{
it: 'should be present for first-party user if no service configured',
isThirdParty: false,
serviceConfigReturns: null,
expected: true,
},
{
it:
'should be present for first-party user if service supports `onLogoutRequest`',
isThirdParty: false,
serviceConfigReturns: { onLogoutRequestProvided: true },
expected: true,
},
{
it:
'should be present for first-party user if service does not support `onLogoutRequest`',
isThirdParty: false,
serviceConfigReturns: { onLogoutRequestProvided: false },
expected: true,
},
{
it: 'should be absent for third-party user if no service configured',
isThirdParty: true,
serviceConfigReturns: null,
expected: false,
},
{
it:
'should be present for third-party user if service supports `onLogoutRequest`',
isThirdParty: true,
serviceConfigReturns: { onLogoutRequestProvided: true },
expected: true,
},
{
it:
'should be absent for third-party user if `onLogoutRequest` not supported',
isThirdParty: true,
serviceConfigReturns: { onLogoutRequestProvided: false },
expected: false,
},
];
tests.forEach(test => {
it(test.it, () => {
fakeIsThirdPartyUser.returns(test.isThirdParty);
fakeServiceConfig.returns(test.serviceConfigReturns);
const wrapper = createUserMenu();
const logOutMenuItem = findMenuItem(wrapper, 'Log out');
assert.equal(logOutMenuItem.exists(), test.expected);
if (test.expected) {
assert.equal(logOutMenuItem.prop('onClick'), fakeOnLogout);
}
});
});
});
});
'use strict';
const { createElement } = require('preact');
const propTypes = require('prop-types');
const bridgeEvents = require('../../shared/bridge-events');
const { isThirdPartyUser } = require('../util/account-id');
const serviceConfig = require('../service-config');
const { withServices } = require('../util/service-context');
const Menu = require('./menu');
const MenuSection = require('./menu-section');
const MenuItem = require('./menu-item');
/**
* A menu with user and account links.
*
* This menu will contain different items depending on service configuration,
* context and whether the user is first- or third-party.
*/
function UserMenu({ auth, bridge, onLogout, serviceUrl, settings }) {
const isThirdParty = isThirdPartyUser(auth.userid, settings.authDomain);
const service = serviceConfig(settings);
const serviceSupports = feature => service && !!service[feature];
const isSelectableProfile =
!isThirdParty || serviceSupports('onProfileRequestProvided');
const isLogoutEnabled =
!isThirdParty || serviceSupports('onLogoutRequestProvided');
const onProfileSelected = () =>
isThirdParty && bridge.call(bridgeEvents.PROFILE_REQUESTED);
// Generate dynamic props for the profile <MenuItem> component
const profileItemProps = (() => {
const props = {};
if (isSelectableProfile) {
if (!isThirdParty) {
props.href = serviceUrl('user', { user: auth.username });
}
props.onClick = onProfileSelected;
}
return props;
})();
const menuLabel = <i className="h-icon-account top-bar__btn" />;
return (
<div className="user-menu">
<Menu label={menuLabel} title={auth.displayName} align="right">
<MenuSection>
<MenuItem
label={auth.displayName}
isDisabled={!isSelectableProfile}
{...profileItemProps}
/>
{!isThirdParty && (
<MenuItem
label="Account settings"
href={serviceUrl('account.settings')}
/>
)}
</MenuSection>
{isLogoutEnabled && (
<MenuSection>
<MenuItem label="Log out" onClick={onLogout} />
</MenuSection>
)}
</Menu>
</div>
);
}
UserMenu.propTypes = {
/* object representing authenticated user and auth status */
auth: propTypes.object.isRequired,
/* onClick callback for the "log out" button */
onLogout: propTypes.func.isRequired,
/* services */
bridge: propTypes.object.isRequired,
serviceUrl: propTypes.func.isRequired,
settings: propTypes.object.isRequired,
};
UserMenu.injectedProps = ['bridge', 'serviceUrl', 'settings'];
module.exports = withServices(UserMenu);
...@@ -198,6 +198,10 @@ function startAngularApp(config) { ...@@ -198,6 +198,10 @@ function startAngularApp(config) {
wrapReactComponent(require('./components/timestamp')) wrapReactComponent(require('./components/timestamp'))
) )
.component('topBar', require('./components/top-bar')) .component('topBar', require('./components/top-bar'))
.component(
'userMenu',
wrapReactComponent(require('./components/user-menu'))
)
.directive('hAutofocus', require('./directive/h-autofocus')) .directive('hAutofocus', require('./directive/h-autofocus'))
.directive('hBranding', require('./directive/h-branding')) .directive('hBranding', require('./directive/h-branding'))
......
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