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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@
"intl": "^1.2.5",
"intl-locales-supported": "^1.0.0",
"jest": "^30.3.0",
"jest-environment-jsdom": "^29.7.0",
"jest-environment-jsdom": "^30.0.0",
"jest-extended": "^5.0.3",
"jest-json-schema": "^6.1.0",
"jest-watch-typeahead": "^3.0.1",
Expand Down
7 changes: 5 additions & 2 deletions src/amo/components/App/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* @flow */
/* global Navigator, navigator */
/* global Navigator, navigator, window */
import config from 'config';
import * as React from 'react';
import { Helmet } from 'react-helmet';
Expand Down Expand Up @@ -32,6 +32,7 @@ import { CLIENT_APP_ANDROID } from 'amo/constants';
import ErrorPage from 'amo/components/ErrorPage';
import translate from 'amo/i18n/translate';
import log from 'amo/logger';
import universalWindow from 'amo/window';
import type { AppState } from 'amo/store';
import type { DispatchFunc } from 'amo/types/redux';
import type { InstalledAddon } from 'amo/reducers/installations';
Expand All @@ -52,6 +53,7 @@ type PropsFromState = {|
type DefaultProps = {|
_addChangeListeners: (callback: Function, mozAddonManager: Object) => any,
_navigator: typeof navigator | null,
_window: typeof window,
mozAddonManager: $PropertyType<MozNavigator, 'mozAddonManager'>,
userAgent: string | null,
|};
Expand All @@ -70,6 +72,7 @@ export class AppBase extends React.Component<Props> {
static defaultProps: DefaultProps = {
_addChangeListeners: addChangeListeners,
_navigator: typeof navigator !== 'undefined' ? navigator : null,
_window: universalWindow,
mozAddonManager: config.get('server')
? {}
: (navigator: MozNavigator).mozAddonManager,
Expand Down Expand Up @@ -148,7 +151,7 @@ export class AppBase extends React.Component<Props> {
<ScrollToTop>
<Helmet defaultTitle={defaultTitle} titleTemplate={titleTemplate} />
<ErrorPage>
<Routes />
<Routes _window={this.props._window} />
</ErrorPage>
</ScrollToTop>
</NestedStatus>
Expand Down
11 changes: 7 additions & 4 deletions src/amo/components/AuthenticateButton/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import translate from 'amo/i18n/translate';
import Button from 'amo/components/Button';
import Icon from 'amo/components/Icon';
import log from 'amo/logger';
import universalWindow from 'amo/window';
import type { ButtonType } from 'amo/components/Button';
import type { AppState } from 'amo/store';
import type { ApiState } from 'amo/reducers/api';
Expand All @@ -23,12 +24,13 @@ import type { ReactRouterLocationType } from 'amo/types/router';

type HandleLogInFunc = (
location: ReactRouterLocationType,
options?: {| _window: typeof window |},
options?: {| _window?: typeof window |},
) => void;

type HandleLogOutFunction = ({| api: ApiState |}) => Promise<void>;

type DefaultProps = {|
_window?: typeof window,
buttonType?: ButtonType,
noIcon?: boolean,
|};
Expand Down Expand Up @@ -58,21 +60,23 @@ type InternalProps = {|

export class AuthenticateButtonBase extends React.Component<InternalProps> {
static defaultProps: DefaultProps = {
_window: universalWindow,
buttonType: 'action',
noIcon: false,
};

onClick: HTMLElementEventHandler = (event: ElementEvent) => {
event.preventDefault();
event.stopPropagation();
const { api, handleLogIn, handleLogOut, location, siteUser } = this.props;
const { _window, api, handleLogIn, handleLogOut, location, siteUser } =
this.props;

invariant(handleLogOut, 'handleLogOut() is undefined');

if (siteUser) {
handleLogOut({ api });
} else {
handleLogIn(location);
handleLogIn(location, { _window });
}
};

Expand Down Expand Up @@ -124,7 +128,6 @@ export const mapStateToProps = (
ownProps: Props,
): PropsFromState => {
const defaultHandleLogIn = (location, { _window = window } = {}) => {
// eslint-disable-next-line no-param-reassign
_window.location.assign(startLoginUrl({ location }));
};

Expand Down
9 changes: 4 additions & 5 deletions src/amo/components/Errors/AuthExpired/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import Link from 'amo/components/Link';
import { replaceStringsWithJSX } from 'amo/i18n/utils';
import translate from 'amo/i18n/translate';
import { logOutUser } from 'amo/reducers/users';
import universalWindow from 'amo/window';
import type { I18nType } from 'amo/types/i18n';
import type { DispatchFunc } from 'amo/types/redux';

Expand All @@ -29,7 +30,7 @@ type InternalProps = {|

export class AuthExpiredBase extends React.Component<InternalProps> {
static defaultProps: DefaultProps = {
_window: typeof window !== 'undefined' ? window : {},
_window: universalWindow,
};

componentDidMount() {
Expand Down Expand Up @@ -72,9 +73,7 @@ export class AuthExpiredBase extends React.Component<InternalProps> {
}
}

const AuthExpired: React.ComponentType<Props> = compose(
connect(),
translate(),
)(AuthExpiredBase);
const AuthExpired: React.ComponentType<{| ...Props, ...DefaultProps |}> =
compose(connect(), translate())(AuthExpiredBase);

export default AuthExpired;
8 changes: 7 additions & 1 deletion src/amo/components/Footer/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* @flow */
/* global window */
import * as React from 'react';
import { compose } from 'redux';
import config from 'config';
Expand All @@ -9,11 +10,13 @@ import ThemePicker from 'amo/components/ThemePicker';
import { makeQueryStringWithUTM, sanitizeHTML } from 'amo/utils';
import translate from 'amo/i18n/translate';
import Icon from 'amo/components/Icon';
import universalWindow from 'amo/window';
import type { I18nType } from 'amo/types/i18n';

import './styles.scss';

type Props = {|
_window?: typeof window,
noLangPicker?: boolean,
noThemePicker?: boolean,
includeGoogleDisclaimer?: boolean,
Expand All @@ -32,18 +35,21 @@ type InternalProps = {|
export class FooterBase extends React.Component<InternalProps> {
static defaultProps: {| ...Props, ...DefaultProps |} = {
_config: config,
_window: universalWindow,
noLangPicker: false,
noThemePicker: false,
};

render(): React.Node {
const {
_config,
_window,
includeGoogleDisclaimer,
i18n,
noLangPicker,
noThemePicker,
} = this.props;

const homepageText = i18n.gettext("Go to Mozilla's homepage");

const footerLinkQueryString = makeQueryStringWithUTM({
Expand Down Expand Up @@ -391,7 +397,7 @@ export class FooterBase extends React.Component<InternalProps> {
/>

<div className="Footer-setting-pickers">
{!noLangPicker && <LanguagePicker />}
{!noLangPicker && <LanguagePicker _window={_window} />}
{!noThemePicker && <ThemePicker />}
</div>
</div>
Expand Down
11 changes: 9 additions & 2 deletions src/amo/components/Page/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* @flow */
/* global window */
import makeClassName from 'classnames';
import config from 'config';
import * as React from 'react';
Expand All @@ -14,6 +15,7 @@ import Footer from 'amo/components/Footer';
import Header from 'amo/components/Header';
import WrongPlatformWarning from 'amo/components/WrongPlatformWarning';
import VPNPromoBanner from 'amo/components/VPNPromoBanner';
import universalWindow from 'amo/window';
import { API_ERRORS_SESSION_EXPIRY, CLIENT_APP_ANDROID } from 'amo/constants';
import { EXPERIMENT_CONFIG } from 'amo/experiments/20210714_amo_vpn_promo';
import log from 'amo/logger';
Expand All @@ -26,6 +28,7 @@ import type { WithExperimentInjectedProps } from 'amo/withExperiment';
import './styles.scss';

type Props = {|
_window?: typeof window,
children: React.Node,
errorHandler?: ErrorHandlerType,
includeGoogleDisclaimerInFooter?: boolean,
Expand All @@ -51,6 +54,7 @@ type InternalProps = {|
export const PageBase = ({
_config = config,
_log = log,
_window = universalWindow,
children,
clientApp,
errorHandler,
Expand All @@ -70,7 +74,7 @@ export const PageBase = ({
errorHandler.capturedError.responseStatusCode === 401 &&
API_ERRORS_SESSION_EXPIRY.includes(errorHandler.capturedError.code)
) {
errorContent = <AuthExpired />;
errorContent = <AuthExpired _window={_window} />;
} else if (
errorHandler.capturedError.responseStatusCode === 401 ||
errorHandler.capturedError.responseStatusCode === 403 ||
Expand Down Expand Up @@ -125,7 +129,10 @@ export const PageBase = ({
</div>
</div>

<Footer includeGoogleDisclaimer={includeGoogleDisclaimerInFooter} />
<Footer
_window={_window}
includeGoogleDisclaimer={includeGoogleDisclaimerInFooter}
/>
</div>
);
};
Expand Down
12 changes: 9 additions & 3 deletions src/amo/components/Routes/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
/* @flow */
/* global window */
import config from 'config';
import * as React from 'react';
import { Route, Switch } from 'react-router-dom';
Expand Down Expand Up @@ -39,14 +40,19 @@ import SimulateSyncError from 'amo/pages/error-simulation/SimulateSyncError';
import About from 'amo/pages/StaticPages/About';
import ReviewGuide from 'amo/pages/StaticPages/ReviewGuide';
import TagPage from 'amo/pages/TagPage';
import universalWindow from 'amo/window';
import type { ConfigType } from 'amo/types/config';

type Props = {
_config?: ConfigType,
_window?: typeof window,
};

// If you add a new route here, check the nginx rules maintained by ops.
const Routes = ({ _config = config }: Props = {}): React.Node => (
const Routes = ({
_config = config,
_window = universalWindow,
}: Props = {}): React.Node => (
<Switch>
<Route exact path="/:lang/about" component={About} />
{/* TODO: Post launch update this URL and redirect see #3374/ */}
Expand Down Expand Up @@ -105,12 +111,12 @@ const Routes = ({ _config = config }: Props = {}): React.Node => (
<Route
exact
path="/:lang/:application(firefox|android)/users/edit"
component={UserProfileEdit}
render={(props) => <UserProfileEdit _window={_window} {...props} />}
/>
<Route
exact
path="/:lang/:application(firefox|android)/user/:userId/edit/"
component={UserProfileEdit}
render={(props) => <UserProfileEdit _window={_window} {...props} />}
/>
<Route
exact
Expand Down
14 changes: 7 additions & 7 deletions src/amo/pages/UserProfileEdit/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { USERS_EDIT, VIEW_CONTEXT_HOME } from 'amo/constants';
import { withFixedErrorHandler } from 'amo/errorHandler';
import log from 'amo/logger';
import translate from 'amo/i18n/translate';
import universalWindow from 'amo/window';
import { sanitizeHTML } from 'amo/utils';
import Button from 'amo/components/Button';
import Card from 'amo/components/Card';
Expand Down Expand Up @@ -70,7 +71,7 @@ type PropsFromState = {|
|};

type DefaultProps = {|
_window: typeof window | Object,
_window: typeof window,
|};

type InternalProps = {|
Expand Down Expand Up @@ -108,7 +109,7 @@ type State = {|

export class UserProfileEditBase extends React.Component<InternalProps, State> {
static defaultProps: DefaultProps = {
_window: typeof window !== 'undefined' ? window : {},
_window: universalWindow,
};

constructor(props: InternalProps) {
Expand Down Expand Up @@ -157,6 +158,7 @@ export class UserProfileEditBase extends React.Component<InternalProps, State> {
} = prevProps;

const {
_window,
clientApp,
currentUser,
dispatch,
Expand Down Expand Up @@ -218,24 +220,22 @@ export class UserProfileEditBase extends React.Component<InternalProps, State> {
toPath = url.parse(toPath).pathname;
if (toPath && !toPath.startsWith('//')) {
try {
this.props._window.location.assign(toPath);
_window.location.assign(toPath);
return;
} catch (error) {
log.warn(`Error redirecting to location: ${toPath}: ${error}`);
}
}
}
this.props._window.location.assign(
`/${lang}/${clientApp}/user/${newUserId}/`,
);
_window.location.assign(`/${lang}/${clientApp}/user/${newUserId}/`);
}

if (
(!prevProps.errorHandler.hasError() &&
this.props.errorHandler.hasError()) ||
(!prevState.successMessage && this.state.successMessage)
) {
this.props._window.scroll(0, 0);
_window.scroll(0, 0);
}
}

Expand Down
18 changes: 8 additions & 10 deletions tests/unit/amo/components/TestAuthenticateButton.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
/* global window */
import * as React from 'react';
import { createEvent, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
Expand All @@ -19,20 +18,15 @@ import {

describe(__filename, () => {
let store;

const savedLocation = window.location;
let fakeWindow;

beforeEach(() => {
store = dispatchClientMetadata().store;
delete window.location;
window.location = Object.assign(new URL('https://example.org'), {
assign: jest.fn(),
});
fakeWindow = { location: { assign: jest.fn() } };
});

afterEach(() => {
jest.clearAllMocks().resetModules();
window.location = savedLocation;
});

const render = ({ location, ...props } = {}) => {
Expand All @@ -42,7 +36,10 @@ describe(__filename, () => {
}),
store,
};
return defaultRender(<AuthenticateButton {...props} />, renderOptions);
return defaultRender(
<AuthenticateButton _window={fakeWindow} {...props} />,
renderOptions,
);
};

it('passes along a className', () => {
Expand Down Expand Up @@ -97,6 +94,7 @@ describe(__filename, () => {
fireEvent(link, clickEvent);
expect(handleLogIn).toHaveBeenCalledWith(
expect.objectContaining({ pathname: location }),
{ _window: fakeWindow },
);
expect(preventDefaultWatcher).toHaveBeenCalled();
});
Expand Down Expand Up @@ -127,7 +125,7 @@ describe(__filename, () => {
expect(startLoginUrl).toHaveBeenCalledWith({
location: expect.objectContaining({ pathname: location }),
});
expect(window.location.assign).toHaveBeenCalledWith(mockLoginURL);
expect(fakeWindow.location.assign).toHaveBeenCalledWith(mockLoginURL);
});

it('calls logOutFromServer on handleLogOut', async () => {
Expand Down
Loading