Skip to content
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
22 changes: 13 additions & 9 deletions client/components/banner/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { connect } from 'react-redux';
import DismissibleCard from 'calypso/blocks/dismissible-card';
import JetpackLogo from 'calypso/components/jetpack-logo';
import TrackComponentView from 'calypso/lib/analytics/track-component-view';
import { addQueryArgs } from 'calypso/lib/url';
import { addQueryArgs, toCalypsoHref } from 'calypso/lib/url';
import { recordTracksEvent } from 'calypso/state/analytics/actions';
import { canCurrentUser } from 'calypso/state/selectors/can-current-user';
import isSiteWPForTeams from 'calypso/state/selectors/is-site-wpforteams';
Expand Down Expand Up @@ -107,23 +107,27 @@ export class Banner extends Component {
getHref() {
const { canUserUpgrade, feature, href, plan, siteSlug, customerType } = this.props;

let computedHref = href;

if ( ! href && siteSlug && canUserUpgrade ) {
if ( customerType ) {
return `/plans/${ siteSlug }?customerType=${ customerType }`;
}
const baseUrl = `/plans/${ siteSlug }`;
if ( feature || plan ) {
return addQueryArgs(

if ( customerType ) {
computedHref = `${ baseUrl }?customerType=${ customerType }`;
} else if ( feature || plan ) {
computedHref = addQueryArgs(
{
feature,
plan,
},
baseUrl
);
} else {
computedHref = baseUrl;
}
return baseUrl;
}
return href;

return toCalypsoHref( computedHref );
}

handleClick = ( e ) => {
Expand Down Expand Up @@ -305,7 +309,7 @@ export class Banner extends Component {
{ secondaryCallToAction && (
<Button
compact={ compactButton }
href={ secondaryHref }
href={ toCalypsoHref( secondaryHref ) }
onClick={ this.handleSecondaryClick }
primary={ false }
>
Expand Down
97 changes: 97 additions & 0 deletions client/components/banner/test/odyssey-href.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* @jest-environment jsdom
*/
jest.mock( '@automattic/calypso-config', () => {
const configApi = () => '';
configApi.isEnabled = jest.fn( ( flag ) => flag === 'is_odyssey' );
return configApi;
} );

jest.mock( 'calypso/state/analytics/actions', () => ( {
recordTracksEvent: jest.fn( () => ( { type: 'ANALYTICS_EVENT_RECORD' } ) ),
} ) );

import { render, screen } from '@testing-library/react';
import { Banner } from '../index';

const props = {
title: 'banner title',
siteSlug: 'example.com',
canUserUpgrade: true,
};

describe( 'Banner hrefs in wp-admin (Odyssey)', () => {
test( 'absolutizes the computed plans href', () => {
const { container } = render( <Banner { ...props } /> );

expect( container.querySelector( 'a' ) ).toHaveAttribute(
'href',
'https://wordpress.com/plans/example.com'
);
} );

test( 'absolutizes the computed plans href with feature and plan args', () => {
const { container } = render(
<Banner { ...props } feature="advanced-seo" plan="business-bundle" />
);

expect( container.querySelector( 'a' ) ).toHaveAttribute(
'href',
'https://wordpress.com/plans/example.com?feature=advanced-seo&plan=business-bundle'
);
} );

test( 'absolutizes the computed plans href with a customerType arg', () => {
const { container } = render( <Banner { ...props } customerType="business" /> );

expect( container.querySelector( 'a' ) ).toHaveAttribute(
'href',
'https://wordpress.com/plans/example.com?customerType=business'
);
} );

test( 'absolutizes a caller-provided href', () => {
const { container } = render( <Banner { ...props } href="/post/example.com" /> );

expect( container.querySelector( 'a' ) ).toHaveAttribute(
'href',
'https://wordpress.com/post/example.com'
);
} );

test( 'absolutizes the call-to-action button href', () => {
render( <Banner { ...props } callToAction="Upgrade" forceHref={ false } /> );

expect( screen.getByRole( 'link', { name: 'Upgrade' } ) ).toHaveAttribute(
'href',
'https://wordpress.com/plans/example.com'
);
} );

test( 'absolutizes the secondary call-to-action href', () => {
render(
<Banner
{ ...props }
callToAction="Upgrade"
secondaryCallToAction="Learn more"
secondaryHref="/support/example.com"
/>
);

expect( screen.getByRole( 'link', { name: 'Learn more' } ) ).toHaveAttribute(
'href',
'https://wordpress.com/support/example.com'
);
} );

test( 'leaves an absolute href untouched', () => {
const { container } = render(
<Banner { ...props } href="https://example.com/wp-admin/admin.php?page=stats" />
);

expect( container.querySelector( 'a' ) ).toHaveAttribute(
'href',
'https://example.com/wp-admin/admin.php?page=stats'
);
} );
} );
1 change: 1 addition & 0 deletions client/lib/url/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ export { addSchemeIfMissing, setUrlScheme } from './scheme-utils';
export { decodeURIIfValid, decodeURIComponentIfValid } from './decode-utils';
export { default as resolveRelativePath } from './resolve-relative-path';
export { pathToUrl } from './path-to-url';
export { default as toCalypsoHref } from './to-calypso-href';
49 changes: 49 additions & 0 deletions client/lib/url/test/to-calypso-href.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* @jest-environment jsdom
*/
jest.mock( '@automattic/calypso-config', () => {
const configApi = () => '';
configApi.isEnabled = jest.fn( () => false );
return configApi;
} );

import config from '@automattic/calypso-config';
import toCalypsoHref from '../to-calypso-href';

const enableOdyssey = ( enabled ) =>
config.isEnabled.mockImplementation( ( flag ) => flag === 'is_odyssey' && enabled );

describe( 'toCalypsoHref', () => {
afterEach( () => {
config.isEnabled.mockReset();
config.isEnabled.mockReturnValue( false );
} );

test( 'absolutizes a root-relative Calypso route in wp-admin (Odyssey)', () => {
enableOdyssey( true );
expect( toCalypsoHref( '/post/example.com' ) ).toBe( 'https://wordpress.com/post/example.com' );
expect( toCalypsoHref( '/plans/example.com?feature=advanced-seo&plan=business-bundle' ) ).toBe(
'https://wordpress.com/plans/example.com?feature=advanced-seo&plan=business-bundle'
);
} );

test( 'leaves absolute and protocol-relative URLs untouched in Odyssey', () => {
enableOdyssey( true );
expect( toCalypsoHref( 'https://example.com/wp-admin/upload.php' ) ).toBe(
'https://example.com/wp-admin/upload.php'
);
expect( toCalypsoHref( '//example.com/path' ) ).toBe( '//example.com/path' );
expect( toCalypsoHref( 'admin.php?page=stats' ) ).toBe( 'admin.php?page=stats' );
} );

test( 'passes empty values through in Odyssey', () => {
enableOdyssey( true );
expect( toCalypsoHref( undefined ) ).toBeUndefined();
expect( toCalypsoHref( null ) ).toBeNull();
} );

test( 'is a no-op outside wp-admin', () => {
enableOdyssey( false );
expect( toCalypsoHref( '/post/example.com' ) ).toBe( '/post/example.com' );
} );
} );
23 changes: 23 additions & 0 deletions client/lib/url/to-calypso-href.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import config from '@automattic/calypso-config';
import { getCalypsoUrl } from '@automattic/calypso-url';

/**
* In wp-admin (Odyssey) a root-relative href is a Calypso route, which the browser would
* resolve against the site's own domain — a 404. Point it at Calypso absolutely there;
* getCalypsoUrl() falls back to https://wordpress.com when the current origin isn't a
* Calypso one, as in wp-admin. Everywhere else (Calypso proper, Jetpack Cloud) this is a
* no-op, so it is safe to apply to any href of unknown origin.
* @param href The href to absolutize.
* @returns The href, absolutized against Calypso only when it is a root-relative route in wp-admin.
*/
export default function toCalypsoHref< T >( href: T ): T | string {
if (
typeof href === 'string' &&
href.startsWith( '/' ) &&
! href.startsWith( '//' ) &&
config.isEnabled( 'is_odyssey' )
) {
return getCalypsoUrl( href );
}
return href;
}
Loading