Begin adding Flow annotations (#2196)
This commit is contained in:
Родитель
e607750626
Коммит
3f41057886
3
.babelrc
3
.babelrc
|
@ -7,6 +7,7 @@
|
|||
"plugins": [
|
||||
"babel-plugin-dedent",
|
||||
"transform-class-properties",
|
||||
"transform-es2015-modules-commonjs"
|
||||
"transform-es2015-modules-commonjs",
|
||||
"transform-flow-strip-types"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
}],
|
||||
// This ensures imports are at the top of the file.
|
||||
"import/imports-first": ["error"],
|
||||
// This catches duplicate exports.
|
||||
// This reports when you accidentally import/export an object twice.
|
||||
"import/no-duplicates": ["error"],
|
||||
// This ensures import statements never provide a file extension in the path.
|
||||
"import/extensions": ["error", "never"],
|
||||
|
@ -72,6 +72,8 @@
|
|||
"import/newline-after-import": ["error"],
|
||||
"jsx-a11y/no-static-element-interactions": "off",
|
||||
"no-console": "error",
|
||||
// We use import/no-duplicates instead because it supports Flow types.
|
||||
"no-duplicate-imports": "off",
|
||||
"no-plusplus": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"space-before-function-paren": ["error", "never"],
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
[ignore]
|
||||
# Ignore built/minified addons-frontend code.
|
||||
<PROJECT_ROOT>/dist/.*
|
||||
|
||||
# These modules opt into Flow but we don't need to check them.
|
||||
.*/node_modules/babel.*
|
||||
.*/node_modules/react-nested-status
|
||||
.*/node_modules/stylelint
|
||||
|
||||
[include]
|
||||
|
||||
[libs]
|
||||
# TODO: this can go away after
|
||||
# https://github.com/mozilla/addons-frontend/issues/2092
|
||||
./flow/libs/dedent.js.flow
|
||||
|
||||
[options]
|
||||
# This maps all Sass/SCSS imports to a dummy Flow file to suppress import
|
||||
# errors. It's not necessary for Flow to analyze Sass/SCSS files.
|
||||
module.name_mapper.extension='scss' -> '<PROJECT_ROOT>/flow/flowStub.js.flow'
|
||||
module.system=node
|
||||
module.system.node.resolve_dirname=node_modules
|
||||
module.system.node.resolve_dirname=./src
|
||||
log.file=./flow/logs/flow.log
|
||||
suppress_comment= \\(.\\|\n\\)*\\$FLOW_FIXME
|
||||
suppress_comment= \\(.\\|\n\\)*\\$FLOW_IGNORE
|
|
@ -2,6 +2,7 @@
|
|||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
flow/logs/*log*
|
||||
|
||||
# Runtime data
|
||||
pids
|
||||
|
|
66
README.md
66
README.md
|
@ -43,6 +43,8 @@ Generic scripts that don't need env vars. Use these for development:
|
|||
| npm run dev:amo | Starts the dev server and proxy (amo) |
|
||||
| npm run dev:amo:no-proxy| Starts the dev server without proxy (amo) |
|
||||
| npm run dev:disco | Starts the dev server (discovery pane) |
|
||||
| npm run flow:check | Check for Flow errors and exit |
|
||||
| npm run flow:dev | Continuously check for Flow errors |
|
||||
| npm run eslint | Lints the JS |
|
||||
| npm run stylelint | Lints the SCSS |
|
||||
| npm run lint | Runs all the JS + SCSS linters |
|
||||
|
@ -84,6 +86,70 @@ or have `InfoDialog` in their behavior text.
|
|||
Any option after the double dash (`--`) gets sent to `mocha`. Check out
|
||||
[mocha's usage](https://mochajs.org/#usage) for ideas.
|
||||
|
||||
### Flow
|
||||
|
||||
There is limited support for using [Flow](https://flowtype.org/)
|
||||
to check for problems in the source code.
|
||||
|
||||
To check for Flow issues during development while you edit files, run:
|
||||
|
||||
npm run flow:dev
|
||||
|
||||
If you are new to working with Flow, here are some tips:
|
||||
* Check out the [getting started](https://flow.org/en/docs/getting-started/) guide.
|
||||
* Read through the [web-ext guide](https://github.com/mozilla/web-ext/blob/master/CONTRIBUTING.md#check-for-flow-errors)
|
||||
for hints on how to solve common Flow errors.
|
||||
|
||||
To add flow coverage to a source file, put a `/* @flow */` comment at the top.
|
||||
The more source files you can opt into Flow, the better.
|
||||
|
||||
Here is our Flow manifesto:
|
||||
|
||||
* We use Flow to **declare the intention of our code** and help others
|
||||
refactor it with confidence.
|
||||
Flow also makes it easier to catch mistakes before spending hours in a debugger
|
||||
trying to find out what happened.
|
||||
* Avoid magic [Flow declarations](https://flowtype.org/en/docs/config/libs/)
|
||||
for any *internal* code. Just declare a
|
||||
[type alias](https://flowtype.org/en/docs/types/aliases/) next to the code
|
||||
where it's used and
|
||||
[export/import](https://flow.org/en/docs/types/modules/) it like any other object.
|
||||
* Never import a real JS object just to reference its type. Make a type alias
|
||||
and import that instead.
|
||||
* Never add more type annotations than you need. Flow is really good at
|
||||
inferring types from standard JS code; it will tell you
|
||||
when you need to add explicit annotations.
|
||||
* When a function like `getAllAddons` takes object arguments, call its
|
||||
type object `GetAllAddonsParams`. Example:
|
||||
|
||||
````js
|
||||
type GetAllAddonsParams = {|
|
||||
categoryId: number,
|
||||
|};
|
||||
|
||||
function getAllAddons({ categoryId }: GetAllAddonsParams = {}) {
|
||||
...
|
||||
}
|
||||
````
|
||||
|
||||
* Use [Exact object types](https://flowtype.org/en/docs/types/objects/#toc-exact-object-types)
|
||||
via the pipe syntax (`{| key: ... |}`) when possible. Sometimes the
|
||||
spread operator triggers an error like
|
||||
'Inexact type is incompatible with exact type' but that's a
|
||||
[bug](https://github.com/facebook/flow/issues/2405).
|
||||
You can use the `Exact<T>` workaround from
|
||||
[`src/core/types/util`](https://github.com/mozilla/addons-frontend/blob/master/src/core/types/util.js)
|
||||
if you have to. This is meant as a working replacement for
|
||||
[$Exact<T>](https://flow.org/en/docs/types/utilities/#toc-exact).
|
||||
* Try to avoid loose types like `Object` or `any` but feel free to use
|
||||
them if you are spending too much time declaring types that depend on other
|
||||
types that depend on other types, and so on.
|
||||
* You can add a `$FLOW_FIXME` comment to skip a Flow check if you run
|
||||
into a bug or if you hit something that's making you bang your head on
|
||||
the keyboard. If it's something you think is unfixable then use
|
||||
`$FLOW_IGNORE` instead. Please explain your rationale in the comment and link
|
||||
to a GitHub issue if possible.
|
||||
|
||||
### Code coverage
|
||||
|
||||
The `npm run unittest` command generates a report of how well the unit tests
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
// This file is just a stub that will suppress 'missing file' errors.
|
|
@ -0,0 +1,3 @@
|
|||
The files in this directory (when declared in `[libs]` of `.flowconfig`)
|
||||
will declare global objects. These declarations should be a last resort.
|
||||
Try to export/import types in the actual source code first.
|
|
@ -0,0 +1,6 @@
|
|||
// TODO: this can go away after
|
||||
// https://github.com/mozilla/addons-frontend/issues/2092
|
||||
|
||||
// I'm not sure exactly what this should be. I was using this issue as a guide.
|
||||
// https://github.com/facebook/flow/issues/2616#issuecomment-289257544
|
||||
declare function dedent(params: Array<*>): string;
|
|
@ -0,0 +1 @@
|
|||
Flow server logs are written to this directory.
|
|
@ -14,6 +14,8 @@
|
|||
"dev:amo:no-proxy": "better-npm-run dev:amo:no-proxy",
|
||||
"dev:disco": "better-npm-run dev:disco",
|
||||
"eslint": "eslint .",
|
||||
"flow:check": "flow check",
|
||||
"flow:dev": "chokidar .flowconfig flow/ src/ tests/ -i flow/logs/flow.log -c 'flow status' --initial",
|
||||
"stylelint": "stylelint --syntax scss **/*.scss",
|
||||
"lint": "npm run eslint && npm run stylelint",
|
||||
"servertest": "bin/config-check.js && ADDONS_FRONTEND_BUILD_ALL=1 npm run build && better-npm-run servertest && better-npm-run servertest:amo && better-npm-run servertest:disco && better-npm-run servertest:admin",
|
||||
|
@ -118,7 +120,7 @@
|
|||
}
|
||||
},
|
||||
"test": {
|
||||
"command": "npm run version-check && npm run unittest && npm run servertest && npm run eslint && npm run stylelint",
|
||||
"command": "npm run version-check && npm run flow:check && npm run unittest && npm run servertest && npm run eslint && npm run stylelint",
|
||||
"env": {
|
||||
"NODE_PATH": "./:./src",
|
||||
"NODE_ENV": "test"
|
||||
|
@ -229,6 +231,7 @@
|
|||
"babel-plugin-react-transform": "2.0.2",
|
||||
"babel-plugin-transform-class-properties": "6.18.0",
|
||||
"babel-plugin-transform-decorators-legacy": "1.3.4",
|
||||
"babel-plugin-transform-flow-strip-types": "6.22.0",
|
||||
"babel-plugin-transform-object-rest-spread": "6.20.2",
|
||||
"babel-preset-es2015": "6.24.0",
|
||||
"babel-preset-react": "6.16.0",
|
||||
|
@ -238,6 +241,7 @@
|
|||
"chai": "3.5.0",
|
||||
"chalk": "1.1.3",
|
||||
"cheerio": "0.22.0",
|
||||
"chokidar-cli": "1.2.0",
|
||||
"concurrently": "3.4.0",
|
||||
"cookie": "0.3.1",
|
||||
"css-loader": "0.28.0",
|
||||
|
@ -249,6 +253,7 @@
|
|||
"eslint-plugin-react": "6.10.3",
|
||||
"fetch-mock": "5.9.4",
|
||||
"file-loader": "0.10.1",
|
||||
"flow-bin": "0.38.0",
|
||||
"glob": "7.1.1",
|
||||
"http-proxy": "1.16.2",
|
||||
"json-loader": "0.5.4",
|
||||
|
|
|
@ -1,15 +1,32 @@
|
|||
/* @flow */
|
||||
import { SET_ADDON_REVIEWS, SET_REVIEW } from 'amo/constants';
|
||||
import type { ApiReviewType } from 'amo/api';
|
||||
|
||||
export function denormalizeReview(review) {
|
||||
export type UserReviewType = {|
|
||||
addonId: number,
|
||||
addonSlug: string,
|
||||
body: string,
|
||||
created: Date,
|
||||
id: number,
|
||||
isLatest: boolean,
|
||||
rating: number,
|
||||
title: string,
|
||||
userId: number,
|
||||
userName: string,
|
||||
userUrl: string,
|
||||
versionId: ?number,
|
||||
|};
|
||||
|
||||
export function denormalizeReview(review: ApiReviewType): UserReviewType {
|
||||
return {
|
||||
addonId: review.addon.id,
|
||||
addonSlug: review.addon.slug,
|
||||
body: review.body,
|
||||
created: review.created,
|
||||
title: review.title,
|
||||
id: review.id,
|
||||
isLatest: review.is_latest,
|
||||
rating: review.rating,
|
||||
title: review.title,
|
||||
userId: review.user.id,
|
||||
userName: review.user.name,
|
||||
userUrl: review.user.url,
|
||||
|
@ -18,26 +35,43 @@ export function denormalizeReview(review) {
|
|||
};
|
||||
}
|
||||
|
||||
const setReviewAction = (review) => ({ type: SET_REVIEW, payload: review });
|
||||
export type SetReviewAction = {|
|
||||
type: string,
|
||||
payload: UserReviewType,
|
||||
|};
|
||||
|
||||
export const setReview = (review, reviewOverrides = {}) => {
|
||||
export const setReview = (review: ApiReviewType): SetReviewAction => {
|
||||
if (!review) {
|
||||
throw new Error('review cannot be empty');
|
||||
}
|
||||
return setReviewAction({
|
||||
...denormalizeReview(review),
|
||||
...reviewOverrides,
|
||||
});
|
||||
return { type: SET_REVIEW, payload: denormalizeReview(review) };
|
||||
};
|
||||
|
||||
export const setDenormalizedReview = (review) => {
|
||||
export const setDenormalizedReview = (
|
||||
review: UserReviewType
|
||||
): SetReviewAction => {
|
||||
if (!review) {
|
||||
throw new Error('review cannot be empty');
|
||||
}
|
||||
return setReviewAction(review);
|
||||
return { type: SET_REVIEW, payload: review };
|
||||
};
|
||||
|
||||
export const setAddonReviews = ({ addonSlug, reviews }) => {
|
||||
export type SetAddonReviewsAction = {|
|
||||
type: string,
|
||||
payload: {|
|
||||
addonSlug: string,
|
||||
reviews: Array<UserReviewType>,
|
||||
|},
|
||||
|};
|
||||
|
||||
type SetAddonReviewsParams = {|
|
||||
addonSlug: string,
|
||||
reviews: Array<ApiReviewType>,
|
||||
|};
|
||||
|
||||
export const setAddonReviews = (
|
||||
{ addonSlug, reviews }: SetAddonReviewsParams
|
||||
): SetAddonReviewsAction => {
|
||||
if (!addonSlug) {
|
||||
throw new Error('addonSlug cannot be empty');
|
||||
}
|
||||
|
|
|
@ -1,6 +1,43 @@
|
|||
/* @flow */
|
||||
import { callApi } from 'core/api';
|
||||
import type { ApiStateType } from 'core/reducers/api';
|
||||
import type { ErrorHandlerType } from 'core/errorHandler';
|
||||
import log from 'core/logger';
|
||||
|
||||
export type ApiReviewType = {|
|
||||
addon: {|
|
||||
id: number,
|
||||
slug: string,
|
||||
|},
|
||||
body: string,
|
||||
created: Date,
|
||||
id: number,
|
||||
is_latest: boolean,
|
||||
rating: number,
|
||||
title: string,
|
||||
user: {|
|
||||
id: number,
|
||||
name: string,
|
||||
url: string,
|
||||
|},
|
||||
version: ?{|
|
||||
id: number,
|
||||
|},
|
||||
|};
|
||||
|
||||
// TODO: make a separate function for posting/patching so that we
|
||||
// can type check each one independently.
|
||||
export type SubmitReviewParams = {|
|
||||
addonId?: number,
|
||||
apiState?: ApiStateType,
|
||||
body?: string,
|
||||
errorHandler?: ErrorHandlerType,
|
||||
rating?: number,
|
||||
reviewId?: number,
|
||||
title?: string,
|
||||
versionId?: number,
|
||||
|};
|
||||
|
||||
/*
|
||||
* POST/PATCH an add-on review using the API.
|
||||
*/
|
||||
|
@ -13,10 +50,16 @@ export function submitReview({
|
|||
body,
|
||||
reviewId,
|
||||
...apiCallParams
|
||||
}) {
|
||||
}: SubmitReviewParams): Promise<ApiReviewType> {
|
||||
return new Promise(
|
||||
(resolve) => {
|
||||
const data = { rating, version: versionId, body, title };
|
||||
const review = {
|
||||
addon: undefined,
|
||||
rating,
|
||||
version: versionId,
|
||||
body,
|
||||
title,
|
||||
};
|
||||
let method = 'POST';
|
||||
let endpoint = 'reviews/review';
|
||||
|
||||
|
@ -24,17 +67,17 @@ export function submitReview({
|
|||
endpoint = `${endpoint}/${reviewId}`;
|
||||
method = 'PATCH';
|
||||
// You cannot update the version of an existing review.
|
||||
data.version = undefined;
|
||||
review.version = undefined;
|
||||
} else {
|
||||
if (!addonId) {
|
||||
throw new Error('addonId is required when posting a new review');
|
||||
}
|
||||
data.addon = addonId;
|
||||
review.addon = addonId;
|
||||
}
|
||||
|
||||
resolve(callApi({
|
||||
endpoint,
|
||||
body: data,
|
||||
body: review,
|
||||
method,
|
||||
auth: true,
|
||||
state: apiState,
|
||||
|
@ -43,7 +86,14 @@ export function submitReview({
|
|||
});
|
||||
}
|
||||
|
||||
export function getReviews({ user, addon, ...params } = {}) {
|
||||
type GetReviewsParams = {|
|
||||
addon: number,
|
||||
user: number,
|
||||
|};
|
||||
|
||||
export function getReviews(
|
||||
{ user, addon, ...params }: GetReviewsParams = {}
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
if (!user && !addon) {
|
||||
throw new Error('Either user or addon must be specified');
|
||||
|
@ -62,7 +112,14 @@ export function getReviews({ user, addon, ...params } = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
export function getLatestUserReview({ user, addon } = {}) {
|
||||
type GetLatestReviewParams = {|
|
||||
addon: number,
|
||||
user: number,
|
||||
|};
|
||||
|
||||
export function getLatestUserReview(
|
||||
{ user, addon }: GetLatestReviewParams = {}
|
||||
) {
|
||||
return new Promise((resolve) => {
|
||||
if (!user || !addon) {
|
||||
throw new Error('Both user and addon must be specified');
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
import React, { PropTypes } from 'react';
|
||||
/* @flow */
|
||||
/* global Node */
|
||||
/* eslint-disable react/sort-comp, react/no-unused-prop-types */
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
|
||||
|
@ -18,26 +21,43 @@ import {
|
|||
import translate from 'core/i18n/translate';
|
||||
import log from 'core/logger';
|
||||
import DefaultRating from 'ui/components/Rating';
|
||||
import type { ErrorHandlerType } from 'core/errorHandler';
|
||||
import type { UserReviewType } from 'amo/actions/reviews';
|
||||
import type { SubmitReviewParams } from 'amo/api';
|
||||
import type { UrlFormatParams } from 'core/api';
|
||||
import type { ApiStateType } from 'core/reducers/api';
|
||||
import type { DispatchFunc } from 'core/types/redux';
|
||||
import type { AddonType, AddonVersionType } from 'core/types/addons';
|
||||
|
||||
import './styles.scss';
|
||||
|
||||
type LoadSavedReviewFunc = ({|
|
||||
userId: number,
|
||||
addonId: number,
|
||||
|}) => Promise<any>;
|
||||
|
||||
type SubmitReviewFunc = (SubmitReviewParams) => Promise<void>;
|
||||
|
||||
type RatingManagerProps = {|
|
||||
AddonReview: typeof DefaultAddonReview,
|
||||
AuthenticateButton: typeof DefaultAuthenticateButton,
|
||||
Rating: typeof DefaultRating,
|
||||
addon: AddonType,
|
||||
apiState: ApiStateType,
|
||||
errorHandler: ErrorHandlerType,
|
||||
i18n: Object,
|
||||
loadSavedReview: LoadSavedReviewFunc,
|
||||
location: UrlFormatParams,
|
||||
submitReview: SubmitReviewFunc,
|
||||
userId: number,
|
||||
userReview: UserReviewType,
|
||||
version: AddonVersionType,
|
||||
|};
|
||||
|
||||
export class RatingManagerBase extends React.Component {
|
||||
static propTypes = {
|
||||
AddonReview: PropTypes.node,
|
||||
AuthenticateButton: PropTypes.node,
|
||||
addon: PropTypes.object.isRequired,
|
||||
errorHandler: PropTypes.func.isRequired,
|
||||
apiState: PropTypes.object,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
loadSavedReview: PropTypes.func.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
Rating: PropTypes.node,
|
||||
submitReview: PropTypes.func.isRequired,
|
||||
userId: PropTypes.number,
|
||||
userReview: PropTypes.object,
|
||||
version: PropTypes.object.isRequired,
|
||||
}
|
||||
props: RatingManagerProps;
|
||||
ratingLegend: Node;
|
||||
state: {| showTextEntry: boolean |};
|
||||
|
||||
static defaultProps = {
|
||||
AddonReview: DefaultAddonReview,
|
||||
|
@ -45,7 +65,7 @@ export class RatingManagerBase extends React.Component {
|
|||
Rating: DefaultRating,
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: RatingManagerProps) {
|
||||
super(props);
|
||||
const { loadSavedReview, userId, addon } = props;
|
||||
this.state = { showTextEntry: false };
|
||||
|
@ -55,16 +75,16 @@ export class RatingManagerBase extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
onSelectRating = (rating) => {
|
||||
const { userId, userReview, version } = this.props;
|
||||
onSelectRating = (rating: number) => {
|
||||
const { userReview, version } = this.props;
|
||||
|
||||
const params = {
|
||||
errorHandler: this.props.errorHandler,
|
||||
rating,
|
||||
apiState: this.props.apiState,
|
||||
addonId: this.props.addon.id,
|
||||
reviewId: undefined,
|
||||
versionId: version.id,
|
||||
userId,
|
||||
};
|
||||
|
||||
if (userReview) {
|
||||
|
@ -89,7 +109,12 @@ export class RatingManagerBase extends React.Component {
|
|||
}
|
||||
|
||||
getLogInPrompt(
|
||||
{ addonType }, { validAddonTypes = defaultValidAddonTypes } = {}
|
||||
{ addonType }: {| addonType: string |},
|
||||
{
|
||||
validAddonTypes = defaultValidAddonTypes,
|
||||
}: {|
|
||||
validAddonTypes: typeof defaultValidAddonTypes,
|
||||
|} = {}
|
||||
) {
|
||||
const { i18n } = this.props;
|
||||
switch (addonType) {
|
||||
|
@ -165,7 +190,10 @@ export class RatingManagerBase extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export const mapStateToProps = (state, ownProps) => {
|
||||
// TODO: when all state types are exported, define `state`.
|
||||
export const mapStateToProps = (
|
||||
state: Object, ownProps: RatingManagerProps
|
||||
) => {
|
||||
const userId = state.auth && state.auth.userId;
|
||||
let userReview;
|
||||
|
||||
|
@ -193,7 +221,14 @@ export const mapStateToProps = (state, ownProps) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
type DispatchMappedProps = {|
|
||||
loadSavedReview: LoadSavedReviewFunc,
|
||||
submitReview: SubmitReviewFunc,
|
||||
|}
|
||||
|
||||
export const mapDispatchToProps = (
|
||||
dispatch: DispatchFunc
|
||||
): DispatchMappedProps => ({
|
||||
|
||||
loadSavedReview({ userId, addonId }) {
|
||||
return getLatestUserReview({ user: userId, addon: addonId })
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* @flow */
|
||||
import {
|
||||
ENTITIES_LOADED,
|
||||
LOG_OUT_USER,
|
||||
|
@ -8,7 +9,12 @@ import {
|
|||
SET_USER_AGENT,
|
||||
} from 'core/constants';
|
||||
|
||||
export function setAuthToken(token) {
|
||||
export type SetAuthTokenAction = {|
|
||||
payload: {| token: string |},
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export function setAuthToken(token: string): SetAuthTokenAction {
|
||||
if (!token) {
|
||||
throw new Error('token cannot be falsey');
|
||||
}
|
||||
|
@ -18,11 +24,20 @@ export function setAuthToken(token) {
|
|||
};
|
||||
}
|
||||
|
||||
export function logOutUser() {
|
||||
export type LogOutUserAction = {|
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export function logOutUser(): LogOutUserAction {
|
||||
return { type: LOG_OUT_USER };
|
||||
}
|
||||
|
||||
export function setClientApp(clientApp) {
|
||||
export type SetClientAppAction = {|
|
||||
payload: {| clientApp: string |},
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export function setClientApp(clientApp: string): SetClientAppAction {
|
||||
if (!clientApp) {
|
||||
throw new Error('clientApp cannot be falsey');
|
||||
}
|
||||
|
@ -32,28 +47,48 @@ export function setClientApp(clientApp) {
|
|||
};
|
||||
}
|
||||
|
||||
export function setLang(lang) {
|
||||
export type SetLangAction = {|
|
||||
payload: {| lang: string |},
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export function setLang(lang: string): SetLangAction {
|
||||
return {
|
||||
type: SET_LANG,
|
||||
payload: { lang },
|
||||
};
|
||||
}
|
||||
|
||||
export function setUserAgent(userAgent) {
|
||||
export type SetUserAgentAction = {|
|
||||
payload: {| userAgent: string |},
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export function setUserAgent(userAgent: string): SetUserAgentAction {
|
||||
return {
|
||||
type: SET_USER_AGENT,
|
||||
payload: { userAgent },
|
||||
};
|
||||
}
|
||||
|
||||
export function loadEntities(entities) {
|
||||
export type LoadEntitiesAction = {|
|
||||
payload: {| entities: Array<Object> |},
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export function loadEntities(entities: Array<Object>): LoadEntitiesAction {
|
||||
return {
|
||||
type: ENTITIES_LOADED,
|
||||
payload: { entities },
|
||||
};
|
||||
}
|
||||
|
||||
export function setCurrentUser(username) {
|
||||
export type SetCurrentUserAction = {|
|
||||
payload: {| username: string |},
|
||||
type: string,
|
||||
|};
|
||||
|
||||
export function setCurrentUser(username: string): SetCurrentUserAction {
|
||||
return {
|
||||
type: SET_CURRENT_USER,
|
||||
payload: { username },
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* @flow */
|
||||
/* global fetch */
|
||||
|
||||
import url from 'url';
|
||||
|
@ -8,9 +9,12 @@ import { schema as normalizrSchema, normalize } from 'normalizr';
|
|||
import { oneLine } from 'common-tags';
|
||||
import config from 'config';
|
||||
|
||||
import { initialApiState } from 'core/reducers/api';
|
||||
import { ADDON_TYPE_THEME } from 'core/constants';
|
||||
import log from 'core/logger';
|
||||
import { convertFiltersToQueryParams } from 'core/searchUtils';
|
||||
import type { ErrorHandlerType } from 'core/errorHandler';
|
||||
import type { ApiStateType } from 'core/reducers/api';
|
||||
|
||||
|
||||
const API_BASE = `${config.get('apiHost')}${config.get('apiPath')}`;
|
||||
|
@ -20,7 +24,7 @@ export const addon = new Entity('addons', {}, { idAttribute: 'slug' });
|
|||
export const category = new Entity('categories', {}, { idAttribute: 'slug' });
|
||||
export const user = new Entity('users', {}, { idAttribute: 'username' });
|
||||
|
||||
export function makeQueryString(query) {
|
||||
export function makeQueryString(query: { [key: string]: * }) {
|
||||
const resolvedQuery = { ...query };
|
||||
Object.keys(resolvedQuery).forEach((key) => {
|
||||
const value = resolvedQuery[key];
|
||||
|
@ -33,7 +37,15 @@ export function makeQueryString(query) {
|
|||
return url.format({ query: resolvedQuery });
|
||||
}
|
||||
|
||||
export function createApiError({ apiURL, response, jsonResponse }) {
|
||||
type CreateApiErrorParams = {|
|
||||
apiURL?: string,
|
||||
response: { status: number },
|
||||
jsonResponse?: Object,
|
||||
|};
|
||||
|
||||
export function createApiError(
|
||||
{ apiURL, response, jsonResponse }: CreateApiErrorParams
|
||||
) {
|
||||
let urlId = '[unknown URL]';
|
||||
if (apiURL) {
|
||||
// Strip the host since we already know that.
|
||||
|
@ -42,6 +54,7 @@ export function createApiError({ apiURL, response, jsonResponse }) {
|
|||
urlId = urlId.split('?')[0];
|
||||
}
|
||||
const apiError = new Error(`Error calling: ${urlId}`);
|
||||
// $FLOW_FIXME: turn Error into a custom ApiError class.
|
||||
apiError.response = {
|
||||
apiURL,
|
||||
status: response.status,
|
||||
|
@ -50,10 +63,29 @@ export function createApiError({ apiURL, response, jsonResponse }) {
|
|||
return apiError;
|
||||
}
|
||||
|
||||
type CallApiParams = {|
|
||||
auth?: boolean,
|
||||
body?: Object,
|
||||
credentials?: boolean,
|
||||
endpoint: string,
|
||||
errorHandler?: ErrorHandlerType,
|
||||
method?: 'GET' | 'POST' | 'DELETE' | 'HEAD' | 'OPTIONS' | 'PUT' | 'PATCH',
|
||||
params?: Object,
|
||||
schema?: Object,
|
||||
state?: ApiStateType,
|
||||
|};
|
||||
|
||||
export function callApi({
|
||||
endpoint, schema, params = {}, auth = false, state = {}, method = 'get',
|
||||
body, credentials, errorHandler,
|
||||
}) {
|
||||
endpoint,
|
||||
schema,
|
||||
params = {},
|
||||
auth = false,
|
||||
state = initialApiState,
|
||||
method = 'GET',
|
||||
body,
|
||||
credentials,
|
||||
errorHandler,
|
||||
}: CallApiParams): Promise<any> {
|
||||
if (errorHandler) {
|
||||
errorHandler.clear();
|
||||
}
|
||||
|
@ -63,6 +95,8 @@ export function callApi({
|
|||
// Always make sure the method is upper case so that the browser won't
|
||||
// complain about CORS problems.
|
||||
method: method.toUpperCase(),
|
||||
credentials: undefined,
|
||||
body: undefined,
|
||||
};
|
||||
if (credentials) {
|
||||
options.credentials = 'include';
|
||||
|
@ -79,6 +113,7 @@ export function callApi({
|
|||
// Workaround for https://github.com/bitinn/node-fetch/issues/245
|
||||
const apiURL = utf8.encode(`${API_BASE}/${endpoint}/${queryString}`);
|
||||
|
||||
// $FLOW_FIXME: once everything uses Flow we won't have to use toUpperCase
|
||||
return fetch(apiURL, options)
|
||||
.then((response) => {
|
||||
const contentType = response.headers.get('Content-Type').toLowerCase();
|
||||
|
@ -124,7 +159,16 @@ export function callApi({
|
|||
.then((response) => (schema ? normalize(response, schema) : response));
|
||||
}
|
||||
|
||||
export function search({ api, page, auth = false, filters = {} }) {
|
||||
type SearchParams = {|
|
||||
api: ApiStateType,
|
||||
auth: boolean,
|
||||
filters: Object,
|
||||
page: number,
|
||||
|};
|
||||
|
||||
export function search(
|
||||
{ api, page, auth = false, filters = {} }: SearchParams
|
||||
) {
|
||||
const _filters = { ...filters };
|
||||
if (!_filters.clientApp && api.clientApp) {
|
||||
log.debug(
|
||||
|
@ -158,7 +202,12 @@ export function search({ api, page, auth = false, filters = {} }) {
|
|||
});
|
||||
}
|
||||
|
||||
export function fetchAddon({ api, slug }) {
|
||||
type FetchAddonParams = {|
|
||||
api: ApiStateType,
|
||||
slug: string,
|
||||
|};
|
||||
|
||||
export function fetchAddon({ api, slug }: FetchAddonParams) {
|
||||
return callApi({
|
||||
endpoint: `addons/addon/${slug}`,
|
||||
schema: addon,
|
||||
|
@ -167,15 +216,21 @@ export function fetchAddon({ api, slug }) {
|
|||
});
|
||||
}
|
||||
|
||||
export function login({ api, code, state }) {
|
||||
const params = {};
|
||||
type LoginParams = {|
|
||||
api: ApiStateType,
|
||||
code: string,
|
||||
state: string,
|
||||
|};
|
||||
|
||||
export function login({ api, code, state }: LoginParams) {
|
||||
const params = { config: undefined };
|
||||
const configName = config.get('fxaConfig');
|
||||
if (configName) {
|
||||
params.config = configName;
|
||||
}
|
||||
return callApi({
|
||||
endpoint: 'accounts/login',
|
||||
method: 'post',
|
||||
method: 'POST',
|
||||
body: { code, state },
|
||||
params,
|
||||
state: api,
|
||||
|
@ -183,9 +238,27 @@ export function login({ api, code, state }) {
|
|||
});
|
||||
}
|
||||
|
||||
export function startLoginUrl({ location }) {
|
||||
// These are all possible parameters to url.format()
|
||||
export type UrlFormatParams = {|
|
||||
+auth?: string;
|
||||
+hash?: string;
|
||||
+host?: string;
|
||||
+hostname?: string;
|
||||
+href?: string;
|
||||
+pathname?: string;
|
||||
+port?: string | number;
|
||||
+protocol?: string;
|
||||
+query?: Object;
|
||||
+search?: string;
|
||||
+slashes?: boolean;
|
||||
|};
|
||||
|
||||
export function startLoginUrl({ location }: { location: UrlFormatParams }) {
|
||||
const configName = config.get('fxaConfig');
|
||||
const params = { to: url.format({ ...location }) };
|
||||
const params = {
|
||||
config: undefined,
|
||||
to: url.format({ ...location }),
|
||||
};
|
||||
if (configName) {
|
||||
params.config = configName;
|
||||
}
|
||||
|
@ -193,7 +266,7 @@ export function startLoginUrl({ location }) {
|
|||
return `${API_BASE}/accounts/login/start/${query}`;
|
||||
}
|
||||
|
||||
export function fetchProfile({ api }) {
|
||||
export function fetchProfile({ api }: {| api: ApiStateType |}) {
|
||||
return callApi({
|
||||
endpoint: 'accounts/profile',
|
||||
schema: user,
|
||||
|
@ -202,7 +275,13 @@ export function fetchProfile({ api }) {
|
|||
});
|
||||
}
|
||||
|
||||
export function featured({ api, filters, page }) {
|
||||
type FeaturedParams = {|
|
||||
api: ApiStateType,
|
||||
filters: Object,
|
||||
page: number,
|
||||
|};
|
||||
|
||||
export function featured({ api, filters, page }: FeaturedParams) {
|
||||
return callApi({
|
||||
endpoint: 'addons/featured',
|
||||
params: {
|
||||
|
@ -215,7 +294,7 @@ export function featured({ api, filters, page }) {
|
|||
});
|
||||
}
|
||||
|
||||
export function categories({ api }) {
|
||||
export function categories({ api }: {| api: ApiStateType |}) {
|
||||
return callApi({
|
||||
endpoint: 'addons/categories',
|
||||
schema: { results: [category] },
|
||||
|
@ -223,12 +302,12 @@ export function categories({ api }) {
|
|||
});
|
||||
}
|
||||
|
||||
export function logOutFromServer({ api }) {
|
||||
export function logOutFromServer({ api }: {| api: ApiStateType |}) {
|
||||
return callApi({
|
||||
auth: true,
|
||||
credentials: true,
|
||||
endpoint: 'accounts/session',
|
||||
method: 'delete',
|
||||
method: 'DELETE',
|
||||
state: api,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
/* global window */
|
||||
import React, { PropTypes } from 'react';
|
||||
/* @flow */
|
||||
/* global Event, window */
|
||||
/* eslint-disable react/sort-comp */
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { compose } from 'redux';
|
||||
|
||||
|
@ -8,27 +10,37 @@ import { logOutFromServer, startLoginUrl } from 'core/api';
|
|||
import translate from 'core/i18n/translate';
|
||||
import Button from 'ui/components/Button';
|
||||
import Icon from 'ui/components/Icon';
|
||||
import type { UrlFormatParams } from 'core/api';
|
||||
import type { ApiStateType } from 'core/reducers/api';
|
||||
import type { DispatchFunc } from 'core/types/redux';
|
||||
|
||||
type HandleLogInFunc = (
|
||||
location: UrlFormatParams, options?: {| _window: typeof window |}
|
||||
) => void;
|
||||
|
||||
type HandleLogOutFunc = ({| api: ApiStateType |}) => Promise<void>;
|
||||
|
||||
type AuthenticateButtonProps = {|
|
||||
api: ApiStateType,
|
||||
className?: string,
|
||||
handleLogIn: HandleLogInFunc,
|
||||
handleLogOut: HandleLogOutFunc,
|
||||
i18n: Object,
|
||||
isAuthenticated: boolean,
|
||||
location: UrlFormatParams,
|
||||
logInText?: string,
|
||||
logOutText?: string,
|
||||
noIcon: boolean,
|
||||
|};
|
||||
|
||||
export class AuthenticateButtonBase extends React.Component {
|
||||
static propTypes = {
|
||||
api: PropTypes.object.isRequired,
|
||||
className: PropTypes.string,
|
||||
handleLogIn: PropTypes.func.isRequired,
|
||||
handleLogOut: PropTypes.func.isRequired,
|
||||
i18n: PropTypes.object.isRequired,
|
||||
isAuthenticated: PropTypes.bool.isRequired,
|
||||
location: PropTypes.object.isRequired,
|
||||
logInText: PropTypes.string,
|
||||
logOutText: PropTypes.string,
|
||||
noIcon: PropTypes.boolean,
|
||||
}
|
||||
props: AuthenticateButtonProps;
|
||||
|
||||
static defaultProps = {
|
||||
noIcon: false,
|
||||
}
|
||||
|
||||
onClick = (event) => {
|
||||
onClick = (event: Event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
const {
|
||||
|
@ -57,7 +69,15 @@ export class AuthenticateButtonBase extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
type StateMappedProps = {|
|
||||
api: ApiStateType,
|
||||
isAuthenticated: boolean,
|
||||
handleLogIn: HandleLogInFunc,
|
||||
|};
|
||||
|
||||
export const mapStateToProps = (
|
||||
state: {| api: ApiStateType |}
|
||||
): StateMappedProps => ({
|
||||
api: state.api,
|
||||
isAuthenticated: !!state.api.token,
|
||||
handleLogIn(location, { _window = window } = {}) {
|
||||
|
@ -66,7 +86,13 @@ export const mapStateToProps = (state) => ({
|
|||
},
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = (dispatch) => ({
|
||||
type DispatchMappedProps = {|
|
||||
handleLogOut: HandleLogOutFunc,
|
||||
|};
|
||||
|
||||
export const mapDispatchToProps = (
|
||||
dispatch: DispatchFunc
|
||||
): DispatchMappedProps => ({
|
||||
handleLogOut({ api }) {
|
||||
return logOutFromServer({ api })
|
||||
.then(() => dispatch(logOutUser()));
|
||||
|
|
|
@ -45,6 +45,8 @@ export class ErrorHandler {
|
|||
}
|
||||
}
|
||||
|
||||
export type ErrorHandlerType = typeof ErrorHandler;
|
||||
|
||||
/*
|
||||
* This is a decorator that gives a component the ability to handle errors.
|
||||
*
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* @flow */
|
||||
import config from 'config';
|
||||
import Jed from 'jed';
|
||||
import moment from 'moment';
|
||||
|
@ -13,7 +14,7 @@ const supportedLangs = langs.concat(Object.keys(langMap));
|
|||
const rtlLangs = config.get('rtlLangs');
|
||||
|
||||
|
||||
export function localeToLang(locale, log_ = log) {
|
||||
export function localeToLang(locale?: any, log_?: typeof log = log) {
|
||||
let lang;
|
||||
if (locale && locale.split) {
|
||||
const parts = locale.split('_');
|
||||
|
@ -34,7 +35,7 @@ export function localeToLang(locale, log_ = log) {
|
|||
return lang;
|
||||
}
|
||||
|
||||
export function langToLocale(language, log_ = log) {
|
||||
export function langToLocale(language?: any, log_?: typeof log = log) {
|
||||
let locale;
|
||||
if (language && language.split) {
|
||||
const parts = language.split('-');
|
||||
|
@ -55,23 +56,36 @@ export function langToLocale(language, log_ = log) {
|
|||
return locale;
|
||||
}
|
||||
|
||||
export function normalizeLang(lang) {
|
||||
export function normalizeLang(lang?: string) {
|
||||
return localeToLang(langToLocale(lang));
|
||||
}
|
||||
|
||||
export function normalizeLocale(locale) {
|
||||
export function normalizeLocale(locale: string) {
|
||||
return langToLocale(localeToLang(locale));
|
||||
}
|
||||
|
||||
export function isSupportedLang(lang, { _supportedLangs = supportedLangs } = {}) {
|
||||
type IsSupportedLangOptions = {|
|
||||
_supportedLangs: typeof supportedLangs,
|
||||
|};
|
||||
|
||||
export function isSupportedLang(
|
||||
lang?: string,
|
||||
{ _supportedLangs = supportedLangs }: IsSupportedLangOptions = {}
|
||||
) {
|
||||
return _supportedLangs.includes(lang);
|
||||
}
|
||||
|
||||
export function isValidLang(lang, { _langs = langs } = {}) {
|
||||
type IsValidLangOptions = {|
|
||||
_langs: typeof langs,
|
||||
|};
|
||||
|
||||
export function isValidLang(
|
||||
lang?: string, { _langs = langs }: IsValidLangOptions = {}
|
||||
) {
|
||||
return _langs.includes(lang);
|
||||
}
|
||||
|
||||
export function sanitizeLanguage(langOrLocale) {
|
||||
export function sanitizeLanguage(langOrLocale?: string) {
|
||||
let language = normalizeLang(langOrLocale);
|
||||
// Only look in the un-mapped lang list.
|
||||
if (!isValidLang(language)) {
|
||||
|
@ -81,12 +95,12 @@ export function sanitizeLanguage(langOrLocale) {
|
|||
return language;
|
||||
}
|
||||
|
||||
export function isRtlLang(lang) {
|
||||
export function isRtlLang(lang: string) {
|
||||
const language = sanitizeLanguage(lang);
|
||||
return rtlLangs.includes(language);
|
||||
}
|
||||
|
||||
export function getDirection(lang) {
|
||||
export function getDirection(lang: string) {
|
||||
return isRtlLang(lang) ? 'rtl' : 'ltr';
|
||||
}
|
||||
|
||||
|
@ -105,7 +119,7 @@ function qualityCmp(a, b) {
|
|||
* sorted array of objects. Example object:
|
||||
* { lang: 'pl', quality: 0.7 }
|
||||
*/
|
||||
export function parseAcceptLanguage(header) {
|
||||
export function parseAcceptLanguage(header: string) {
|
||||
// pl,fr-FR;q=0.3,en-US;q=0.1
|
||||
if (!header || !header.split) {
|
||||
return [];
|
||||
|
@ -129,6 +143,10 @@ export function parseAcceptLanguage(header) {
|
|||
return langList;
|
||||
}
|
||||
|
||||
type GetLangFromHeaderOptions = {|
|
||||
_supportedLangs?: Object,
|
||||
|};
|
||||
|
||||
|
||||
/*
|
||||
* Given an accept-language header and a list of currently
|
||||
|
@ -137,7 +155,9 @@ export function parseAcceptLanguage(header) {
|
|||
* Note: this doesn't map languages e.g. pt -> pt-PT. Use sanitizeLanguage for that.
|
||||
*
|
||||
*/
|
||||
export function getLangFromHeader(acceptLanguage, { _supportedLangs } = {}) {
|
||||
export function getLangFromHeader(
|
||||
acceptLanguage: string, { _supportedLangs }: GetLangFromHeaderOptions = {}
|
||||
) {
|
||||
let userLang;
|
||||
if (acceptLanguage) {
|
||||
const langList = parseAcceptLanguage(acceptLanguage);
|
||||
|
@ -156,13 +176,18 @@ export function getLangFromHeader(acceptLanguage, { _supportedLangs } = {}) {
|
|||
return normalizeLang(userLang);
|
||||
}
|
||||
|
||||
type GetLanguageParams = {|
|
||||
lang: string,
|
||||
acceptLanguage: string,
|
||||
|};
|
||||
|
||||
/*
|
||||
* Check validity of language:
|
||||
* - If invalid, fall-back to accept-language.
|
||||
* - Return object with lang and isLangFromHeader hint.
|
||||
*
|
||||
*/
|
||||
export function getLanguage({ lang, acceptLanguage } = {}) {
|
||||
export function getLanguage({ lang, acceptLanguage }: GetLanguageParams = {}) {
|
||||
let userLang = lang;
|
||||
let isLangFromHeader = false;
|
||||
// If we don't have a supported userLang yet try accept-language.
|
||||
|
@ -177,7 +202,7 @@ export function getLanguage({ lang, acceptLanguage } = {}) {
|
|||
}
|
||||
|
||||
// moment uses locales like "en-gb" whereas we use "en_GB".
|
||||
export function makeMomentLocale(locale) {
|
||||
export function makeMomentLocale(locale: string) {
|
||||
return locale.replace('_', '-').toLowerCase();
|
||||
}
|
||||
|
||||
|
@ -191,12 +216,32 @@ function oneLineTranslationString(translationKey) {
|
|||
return translationKey;
|
||||
}
|
||||
|
||||
type I18nConfig = {|
|
||||
// The following keys configure Jed.
|
||||
// See http://messageformat.github.io/Jed/
|
||||
domain: string,
|
||||
locale_data: {
|
||||
[domain: string]: {
|
||||
'': { // an empty string configures the domain.
|
||||
domain: string,
|
||||
lang: string,
|
||||
plural_forms: string,
|
||||
},
|
||||
[message: string]: Array<string>,
|
||||
},
|
||||
},
|
||||
// This is our custom configuration for moment.
|
||||
_momentDefineLocale?: Function,
|
||||
|};
|
||||
|
||||
// Create an i18n object with a translated moment object available we can
|
||||
// use for translated dates across the app.
|
||||
export function makeI18n(i18nData, lang, _Jed = Jed) {
|
||||
export function makeI18n(i18nData: I18nConfig, lang: string, _Jed: Jed = Jed) {
|
||||
const i18n = new _Jed(i18nData);
|
||||
i18n.lang = lang;
|
||||
|
||||
// TODO: move all of this to an I18n class that extends Jed so that we
|
||||
// can type-check all the components that rely on the i18n object.
|
||||
i18n.formatNumber = (number) => number.toLocaleString(lang);
|
||||
|
||||
// This adds the correct moment locale for the active locale so we can get
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* @flow */
|
||||
import UAParser from 'ua-parser-js';
|
||||
|
||||
import {
|
||||
|
@ -7,8 +8,44 @@ import {
|
|||
SET_CLIENT_APP,
|
||||
SET_USER_AGENT,
|
||||
} from 'core/constants';
|
||||
import type {
|
||||
SetAuthTokenAction,
|
||||
LogOutUserAction,
|
||||
SetClientAppAction,
|
||||
SetLangAction,
|
||||
SetUserAgentAction,
|
||||
} from 'core/actions/index';
|
||||
import type { Exact } from 'core/types/util';
|
||||
|
||||
export default function api(state = {}, action) {
|
||||
type UserAgentInfoType = {|
|
||||
browser: string,
|
||||
os: string,
|
||||
|};
|
||||
|
||||
export type ApiStateType = {
|
||||
clientApp: ?string,
|
||||
lang: ?string,
|
||||
token: ?string,
|
||||
userAgent: ?string,
|
||||
userAgentInfo: ?UserAgentInfoType,
|
||||
};
|
||||
|
||||
export const initialApiState = {
|
||||
clientApp: null,
|
||||
lang: null,
|
||||
token: null,
|
||||
userAgent: null,
|
||||
userAgentInfo: null,
|
||||
};
|
||||
|
||||
export default function api(
|
||||
state: Exact<ApiStateType> = initialApiState,
|
||||
action: SetAuthTokenAction
|
||||
& SetLangAction
|
||||
& SetClientAppAction
|
||||
& SetUserAgentAction
|
||||
& LogOutUserAction
|
||||
): Exact<ApiStateType> {
|
||||
switch (action.type) {
|
||||
case SET_AUTH_TOKEN:
|
||||
return { ...state, token: action.payload.token };
|
||||
|
@ -27,11 +64,7 @@ export default function api(state = {}, action) {
|
|||
};
|
||||
}
|
||||
case LOG_OUT_USER:
|
||||
{
|
||||
const newState = { ...state };
|
||||
delete newState.token;
|
||||
return newState;
|
||||
}
|
||||
return { ...state, token: null };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,68 @@
|
|||
/* @flow */
|
||||
export type AddonVersionType = {|
|
||||
channel: string,
|
||||
edit_url: string,
|
||||
files: Array<Object>,
|
||||
id: number,
|
||||
// The `text` property is omitted from addon.current_version.license.
|
||||
license: { name: string, url: string },
|
||||
reviewed: Date,
|
||||
version: string,
|
||||
|};
|
||||
|
||||
export type AddonAuthorType = {|
|
||||
name: string,
|
||||
url: string,
|
||||
|};
|
||||
|
||||
export type AddonType = {|
|
||||
authors: Array<AddonAuthorType>,
|
||||
average_daily_users: number,
|
||||
categories: Object,
|
||||
compatibility: Object,
|
||||
current_version: AddonVersionType,
|
||||
default_locale: string,
|
||||
description: string,
|
||||
edit_url: string,
|
||||
guid: string,
|
||||
has_eula: boolean,
|
||||
has_privacy_policy: boolean,
|
||||
homepage: string,
|
||||
icon_url: string,
|
||||
id: number,
|
||||
is_disabled: boolean,
|
||||
is_experimental: boolean,
|
||||
is_source_public: boolean,
|
||||
last_updated: Date,
|
||||
latest_unlisted_version: ?AddonVersionType,
|
||||
name: string,
|
||||
previews: Array<Object>,
|
||||
public_stats: boolean,
|
||||
ratings: {|
|
||||
average: number,
|
||||
count: number,
|
||||
|},
|
||||
review_url: string,
|
||||
slug: string,
|
||||
status:
|
||||
| 'beta'
|
||||
| 'lite'
|
||||
| 'public'
|
||||
| 'deleted'
|
||||
| 'pending'
|
||||
| 'disabled'
|
||||
| 'rejected'
|
||||
| 'nominated'
|
||||
| 'incomplete'
|
||||
| 'unreviewed'
|
||||
| 'lite-nominated'
|
||||
| 'review-pending',
|
||||
summary: string,
|
||||
support_email: string,
|
||||
support_url: string,
|
||||
tags: Array<string>,
|
||||
theme_data: Object,
|
||||
type: string,
|
||||
url: string,
|
||||
weekly_downloads: number,
|
||||
|};
|
|
@ -0,0 +1,7 @@
|
|||
/* @flow */
|
||||
|
||||
// This defines some Redux interfaces that we want to depend on.
|
||||
// It may be possible to use an official library for this.
|
||||
// See: https://github.com/reactjs/react-redux/pull/389
|
||||
// and: https://github.com/reactjs/redux/pull/1887/files#diff-46d86d39c8da613247f843ee8ca43ebc
|
||||
export type DispatchFunc = (action: Object) => void;
|
|
@ -0,0 +1,30 @@
|
|||
/* @flow */
|
||||
/* global $Shape */
|
||||
|
||||
// TODO: This can go away when https://github.com/facebook/flow/issues/2405
|
||||
// is fixed.
|
||||
//
|
||||
// Exact is a fixed version of the $Exact object type, which you typically
|
||||
// see used as pipes within an object type:
|
||||
// https://flowtype.org/en/docs/types/objects/#toc-exact-object-types
|
||||
// It is available as a generic utility called $Exact<Object> too:
|
||||
// https://flow.org/en/docs/types/utilities/#toc-exact
|
||||
//
|
||||
// At the time of this writing, you may need to use this workaround if you
|
||||
// want to use the spread operator to merge objects together. See:
|
||||
// https://github.com/facebook/flow/issues/2405#issuecomment-274073091
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// Let's say you have a loosely defined parameter type for a function like this:
|
||||
//
|
||||
// type GetAllAddonsParams = {
|
||||
// categoryId: number,
|
||||
// };
|
||||
//
|
||||
// You can make sure this function accepts *only* these parameters like this:
|
||||
//
|
||||
// function getAllAddons({ categoryId }: Exact<GetAllAddonsParams> = {}) {
|
||||
// ...
|
||||
// }
|
||||
export type Exact<T> = T & $Shape<T>;
|
|
@ -21,6 +21,7 @@ import translate from 'core/i18n/translate';
|
|||
import { loadEntities } from 'core/actions';
|
||||
import * as coreApi from 'core/api';
|
||||
import { denormalizeAddon } from 'core/reducers/addons';
|
||||
import { initialApiState } from 'core/reducers/api';
|
||||
import I18nProvider from 'core/i18n/Provider';
|
||||
import Rating from 'ui/components/Rating';
|
||||
import { fakeAddon, fakeReview } from 'tests/client/amo/helpers';
|
||||
|
@ -254,7 +255,7 @@ describe('amo/components/AddonReviewList', () => {
|
|||
mockCoreApi
|
||||
.expects('fetchAddon')
|
||||
.once()
|
||||
.withArgs({ slug: addonSlug, api: {} })
|
||||
.withArgs({ slug: addonSlug, api: { ...initialApiState } })
|
||||
.returns(Promise.resolve(createFetchAddonResult(fakeAddon)));
|
||||
|
||||
mockAmoApi
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
ADDON_TYPE_THEME,
|
||||
} from 'core/constants';
|
||||
import I18nProvider from 'core/i18n/Provider';
|
||||
import { initialApiState } from 'core/reducers/api';
|
||||
import * as amoApi from 'amo/api';
|
||||
import createStore from 'amo/store';
|
||||
import { setReview } from 'amo/actions/reviews';
|
||||
|
@ -80,7 +81,6 @@ describe('RatingManager', () => {
|
|||
apiState: { ...signedInApiState, token: 'new-token' },
|
||||
version: { id: 321 },
|
||||
addon: { ...fakeAddon, id: 12345, slug: 'some-slug' },
|
||||
userId: 92345,
|
||||
});
|
||||
return root.onSelectRating(5)
|
||||
.then(() => {
|
||||
|
@ -91,7 +91,6 @@ describe('RatingManager', () => {
|
|||
assert.equal(call.apiState.token, 'new-token');
|
||||
assert.equal(call.addonId, 12345);
|
||||
assert.equal(call.errorHandler, errorHandler);
|
||||
assert.equal(call.userId, 92345);
|
||||
assert.strictEqual(call.reviewId, undefined);
|
||||
});
|
||||
});
|
||||
|
@ -374,7 +373,7 @@ describe('RatingManager', () => {
|
|||
});
|
||||
|
||||
it('sets an empty apiState when not signed in', () => {
|
||||
assert.deepEqual(getMappedProps().apiState, {});
|
||||
assert.deepEqual(getMappedProps().apiState, { ...initialApiState });
|
||||
});
|
||||
|
||||
it('sets an empty userId when not signed in', () => {
|
||||
|
@ -395,7 +394,7 @@ describe('RatingManager', () => {
|
|||
it('sets a user review to the latest matching one in state', () => {
|
||||
signIn({ userId: fakeReview.user.id });
|
||||
|
||||
const action = setReview(fakeReview, { isLatest: true });
|
||||
const action = setReview({ ...fakeReview, is_latest: true });
|
||||
store.dispatch(action);
|
||||
const dispatchedReview = action.payload;
|
||||
|
||||
|
@ -411,9 +410,13 @@ describe('RatingManager', () => {
|
|||
signIn({ userId: userIdOne });
|
||||
|
||||
// Save a review for user two.
|
||||
store.dispatch(setReview(fakeReview, {
|
||||
isLatest: true,
|
||||
userId: userIdTwo,
|
||||
store.dispatch(setReview({
|
||||
...fakeReview,
|
||||
is_latest: true,
|
||||
user: {
|
||||
...fakeReview.user,
|
||||
id: userIdTwo,
|
||||
},
|
||||
rating: savedRating,
|
||||
}));
|
||||
|
||||
|
@ -435,18 +438,18 @@ describe('RatingManager', () => {
|
|||
signIn({ userId: fakeReview.user.id });
|
||||
|
||||
function createReview(overrides) {
|
||||
const action = setReview(fakeReview, overrides);
|
||||
const action = setReview({ ...fakeReview, ...overrides });
|
||||
store.dispatch(action);
|
||||
return action.payload;
|
||||
}
|
||||
|
||||
createReview({
|
||||
id: 1,
|
||||
isLatest: false,
|
||||
is_latest: false,
|
||||
});
|
||||
const latestReview = createReview({
|
||||
id: 2,
|
||||
isLatest: true,
|
||||
is_latest: true,
|
||||
});
|
||||
|
||||
assert.deepEqual(getMappedProps().userReview, latestReview);
|
||||
|
|
|
@ -3,13 +3,37 @@ import reviews, { initialState } from 'amo/reducers/reviews';
|
|||
import { fakeAddon, fakeReview } from 'tests/client/amo/helpers';
|
||||
|
||||
describe('amo.reducers.reviews', () => {
|
||||
function setFakeReview({
|
||||
userId = fakeReview.user.id,
|
||||
addonId = fakeReview.addon.id,
|
||||
versionId = fakeReview.version.id,
|
||||
...overrides } = {}
|
||||
) {
|
||||
return setReview({
|
||||
...fakeReview,
|
||||
user: {
|
||||
...fakeReview.user,
|
||||
id: userId,
|
||||
},
|
||||
addon: {
|
||||
...fakeReview.addon,
|
||||
id: addonId,
|
||||
},
|
||||
version: {
|
||||
...fakeReview.version,
|
||||
id: versionId,
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
|
||||
it('defaults to an empty object', () => {
|
||||
assert.deepEqual(reviews(undefined, { type: 'SOME_OTHER_ACTION' }),
|
||||
initialState);
|
||||
});
|
||||
|
||||
it('stores a user review', () => {
|
||||
const action = setReview(fakeReview);
|
||||
const action = setFakeReview();
|
||||
const state = reviews(undefined, action);
|
||||
const storedReview =
|
||||
state[fakeReview.user.id][fakeReview.addon.id][fakeReview.id];
|
||||
|
@ -32,21 +56,21 @@ describe('amo.reducers.reviews', () => {
|
|||
it('preserves existing user rating data', () => {
|
||||
let state;
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 1,
|
||||
userId: 1,
|
||||
addonId: 1,
|
||||
rating: 1,
|
||||
}));
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 2,
|
||||
userId: 1,
|
||||
addonId: 2,
|
||||
rating: 5,
|
||||
}));
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 3,
|
||||
userId: 2,
|
||||
addonId: 2,
|
||||
|
@ -64,17 +88,17 @@ describe('amo.reducers.reviews', () => {
|
|||
const userId = fakeReview.user.id;
|
||||
const addonId = fakeReview.addon.id;
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 1,
|
||||
versionId: 1,
|
||||
}));
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 2,
|
||||
versionId: 2,
|
||||
}));
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 3,
|
||||
versionId: 3,
|
||||
}));
|
||||
|
@ -87,7 +111,7 @@ describe('amo.reducers.reviews', () => {
|
|||
|
||||
it('preserves unrelated state', () => {
|
||||
let state = { ...initialState, somethingUnrelated: 'erp' };
|
||||
state = reviews(state, setReview(fakeReview));
|
||||
state = reviews(state, setFakeReview());
|
||||
assert.equal(state.somethingUnrelated, 'erp');
|
||||
});
|
||||
|
||||
|
@ -96,19 +120,19 @@ describe('amo.reducers.reviews', () => {
|
|||
const userId = fakeReview.user.id;
|
||||
let state;
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 1,
|
||||
isLatest: true,
|
||||
is_latest: true,
|
||||
}));
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 2,
|
||||
isLatest: true,
|
||||
is_latest: true,
|
||||
}));
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 3,
|
||||
isLatest: true,
|
||||
is_latest: true,
|
||||
}));
|
||||
|
||||
// Make sure only the newest submitted one is the latest:
|
||||
|
@ -122,14 +146,14 @@ describe('amo.reducers.reviews', () => {
|
|||
const userId = fakeReview.user.id;
|
||||
let state;
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 1,
|
||||
isLatest: true,
|
||||
is_latest: true,
|
||||
}));
|
||||
|
||||
state = reviews(state, setReview(fakeReview, {
|
||||
state = reviews(state, setFakeReview({
|
||||
id: 2,
|
||||
isLatest: false,
|
||||
is_latest: false,
|
||||
}));
|
||||
|
||||
assert.equal(state[userId][addonId][1].isLatest, true);
|
||||
|
|
|
@ -18,10 +18,11 @@ describe('amo.api', () => {
|
|||
// These are all the default values for fields that can be posted to the
|
||||
// endpoint.
|
||||
const defaultParams = {
|
||||
rating: undefined,
|
||||
version: undefined,
|
||||
addon: undefined,
|
||||
body: undefined,
|
||||
rating: undefined,
|
||||
title: undefined,
|
||||
version: undefined,
|
||||
};
|
||||
const baseParams = {
|
||||
apiState: { ...signedInApiState, token: 'new-token' },
|
||||
|
|
|
@ -5,6 +5,7 @@ import createStore from 'amo/store';
|
|||
import * as featuredActions from 'amo/actions/featured';
|
||||
import * as landingActions from 'amo/actions/landing';
|
||||
import * as api from 'core/api';
|
||||
import { initialApiState } from 'core/reducers/api';
|
||||
import {
|
||||
ADDON_TYPE_EXTENSION,
|
||||
ADDON_TYPE_THEME,
|
||||
|
@ -39,7 +40,9 @@ describe('amo/utils', () => {
|
|||
mockApi
|
||||
.expects('featured')
|
||||
.once()
|
||||
.withArgs({ api: {}, filters: { addonType, page_size: 25 } })
|
||||
.withArgs({
|
||||
api: { ...initialApiState }, filters: { addonType, page_size: 25 },
|
||||
})
|
||||
.returns(Promise.resolve({ entities, result }));
|
||||
|
||||
return loadFeaturedAddons({ store, params: ownProps.params })
|
||||
|
@ -61,13 +64,15 @@ describe('amo/utils', () => {
|
|||
mockApi
|
||||
.expects('featured')
|
||||
.once()
|
||||
.withArgs({ api: {}, filters: { addonType, page_size: 4 } })
|
||||
.withArgs({
|
||||
api: { ...initialApiState }, filters: { addonType, page_size: 4 },
|
||||
})
|
||||
.returns(Promise.resolve({ entities, result }));
|
||||
mockApi
|
||||
.expects('search')
|
||||
.once()
|
||||
.withArgs({
|
||||
api: {},
|
||||
api: { ...initialApiState },
|
||||
filters: { addonType, page_size: 4, sort: SEARCH_SORT_TOP_RATED },
|
||||
page: 1,
|
||||
})
|
||||
|
@ -76,7 +81,7 @@ describe('amo/utils', () => {
|
|||
.expects('search')
|
||||
.once()
|
||||
.withArgs({
|
||||
api: {},
|
||||
api: { ...initialApiState },
|
||||
filters: { addonType, page_size: 4, sort: SEARCH_SORT_POPULAR },
|
||||
page: 1,
|
||||
})
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global Response, window */
|
||||
import querystring from 'querystring';
|
||||
|
||||
import config from 'config';
|
||||
import utf8 from 'utf8';
|
||||
|
||||
|
@ -51,7 +53,7 @@ describe('api', () => {
|
|||
it('transforms method to upper case', () => {
|
||||
mockWindow.expects('fetch')
|
||||
.withArgs(`${apiHost}/api/v3/resource/`, {
|
||||
method: 'GET', headers: {},
|
||||
body: undefined, credentials: undefined, method: 'GET', headers: {},
|
||||
})
|
||||
.once()
|
||||
.returns(createApiResponse());
|
||||
|
@ -63,7 +65,7 @@ describe('api', () => {
|
|||
const endpoint = 'diccionario-español-venezuela';
|
||||
mockWindow.expects('fetch')
|
||||
.withArgs(utf8.encode(`${apiHost}/api/v3/${endpoint}/`), {
|
||||
method: 'GET', headers: {},
|
||||
body: undefined, credentials: undefined, method: 'GET', headers: {},
|
||||
})
|
||||
.once()
|
||||
.returns(createApiResponse());
|
||||
|
@ -156,7 +158,7 @@ describe('api', () => {
|
|||
|
||||
mockWindow.expects('fetch')
|
||||
.withArgs(`${apiHost}/api/v3/resource/`, {
|
||||
method: 'GET', headers: {},
|
||||
body: undefined, credentials: undefined, method: 'GET', headers: {},
|
||||
})
|
||||
.once()
|
||||
.returns(response);
|
||||
|
@ -395,9 +397,12 @@ describe('api', () => {
|
|||
|
||||
it('sets the lang and slug', () => {
|
||||
mockWindow.expects('fetch')
|
||||
.withArgs(
|
||||
`${apiHost}/api/v3/addons/addon/foo/?lang=en-US`,
|
||||
{ headers: {}, method: 'GET' })
|
||||
.withArgs(`${apiHost}/api/v3/addons/addon/foo/?lang=en-US`, {
|
||||
body: undefined,
|
||||
credentials: undefined,
|
||||
headers: {},
|
||||
method: 'GET',
|
||||
})
|
||||
.once()
|
||||
.returns(mockResponse());
|
||||
return api.fetchAddon({ api: { lang: 'en-US' }, slug: 'foo' })
|
||||
|
@ -417,9 +422,12 @@ describe('api', () => {
|
|||
it('fails when the add-on is not found', () => {
|
||||
mockWindow
|
||||
.expects('fetch')
|
||||
.withArgs(
|
||||
`${apiHost}/api/v3/addons/addon/foo/?lang=en-US`,
|
||||
{ headers: {}, method: 'GET' })
|
||||
.withArgs(`${apiHost}/api/v3/addons/addon/foo/?lang=en-US`, {
|
||||
body: undefined,
|
||||
credentials: undefined,
|
||||
headers: {},
|
||||
method: 'GET',
|
||||
})
|
||||
.once()
|
||||
.returns(mockResponse({ ok: false }));
|
||||
return api.fetchAddon({ api: { lang: 'en-US' }, slug: 'foo' })
|
||||
|
@ -434,9 +442,12 @@ describe('api', () => {
|
|||
const token = userAuthToken();
|
||||
mockWindow
|
||||
.expects('fetch')
|
||||
.withArgs(
|
||||
`${apiHost}/api/v3/addons/addon/bar/?lang=en-US`,
|
||||
{ headers: { authorization: `Bearer ${token}` }, method: 'GET' })
|
||||
.withArgs(`${apiHost}/api/v3/addons/addon/bar/?lang=en-US`, {
|
||||
body: undefined,
|
||||
credentials: undefined,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
method: 'GET',
|
||||
})
|
||||
.once()
|
||||
.returns(mockResponse());
|
||||
return api.fetchAddon({ api: { lang: 'en-US', token }, slug: 'bar' })
|
||||
|
@ -522,6 +533,8 @@ describe('api', () => {
|
|||
mockWindow
|
||||
.expects('fetch')
|
||||
.withArgs(`${apiHost}/api/v3/accounts/profile/?lang=en-US`, {
|
||||
body: undefined,
|
||||
credentials: undefined,
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
method: 'GET',
|
||||
})
|
||||
|
@ -539,19 +552,19 @@ describe('api', () => {
|
|||
});
|
||||
|
||||
describe('startLoginUrl', () => {
|
||||
const getStartLoginQs = (location) =>
|
||||
querystring.parse(api.startLoginUrl({ location }).split('?')[1]);
|
||||
|
||||
it('includes the next path', () => {
|
||||
const location = { pathname: '/foo', query: { bar: 'BAR' } };
|
||||
assert.equal(
|
||||
api.startLoginUrl({ location }),
|
||||
`${apiHost}/api/v3/accounts/login/start/?to=%2Ffoo%3Fbar%3DBAR`);
|
||||
assert.deepEqual(getStartLoginQs(location), { to: '/foo?bar=BAR' });
|
||||
});
|
||||
|
||||
it('includes the next path the config if set', () => {
|
||||
sinon.stub(config, 'get').withArgs('fxaConfig').returns('my-config');
|
||||
const location = { pathname: '/foo' };
|
||||
assert.equal(
|
||||
api.startLoginUrl({ location }),
|
||||
`${apiHost}/api/v3/accounts/login/start/?to=%2Ffoo&config=my-config`);
|
||||
assert.deepEqual(
|
||||
getStartLoginQs(location), { to: '/foo', config: 'my-config' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -587,6 +600,7 @@ describe('api', () => {
|
|||
const mockResponse = createApiResponse({ jsonData: { ok: true } });
|
||||
mockWindow.expects('fetch')
|
||||
.withArgs(`${apiHost}/api/v3/accounts/session/?lang=en-US`, {
|
||||
body: undefined,
|
||||
credentials: 'include',
|
||||
headers: { authorization: 'Bearer secret-token' },
|
||||
method: 'DELETE',
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import UAParser from 'ua-parser-js';
|
||||
|
||||
import * as actions from 'core/actions';
|
||||
import api from 'core/reducers/api';
|
||||
import api, { initialApiState } from 'core/reducers/api';
|
||||
import { signedInApiState, userAgents, userAuthToken }
|
||||
from 'tests/client/helpers';
|
||||
|
||||
|
@ -20,9 +20,9 @@ describe('api reducer', () => {
|
|||
});
|
||||
|
||||
it('clears the auth token on log out', () => {
|
||||
const expectedState = { ...signedInApiState };
|
||||
assert.ok(expectedState.token, 'signed in state did not have a token');
|
||||
delete expectedState.token;
|
||||
const state = { ...signedInApiState };
|
||||
assert.ok(state.token, 'signed in state did not have a token');
|
||||
const expectedState = { ...state, token: null };
|
||||
assert.deepEqual(
|
||||
api(signedInApiState, actions.logOutUser()), expectedState);
|
||||
});
|
||||
|
@ -82,6 +82,7 @@ describe('api reducer', () => {
|
|||
});
|
||||
|
||||
it('defaults to an empty object', () => {
|
||||
assert.deepEqual(api(undefined, { type: 'UNRELATED' }), {});
|
||||
assert.deepEqual(
|
||||
api(undefined, { type: 'UNRELATED' }), { ...initialApiState });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import createStore from 'amo/store';
|
||||
import * as searchActions from 'core/actions/search';
|
||||
import * as api from 'core/api';
|
||||
import { initialApiState } from 'core/reducers/api';
|
||||
import { ADDON_TYPE_THEME } from 'core/constants';
|
||||
import { loadByCategoryIfNeeded, mapStateToProps } from 'core/searchUtils';
|
||||
|
||||
|
@ -52,7 +53,7 @@ describe('searchUtils loadByCategoryIfNeeded()', () => {
|
|||
mockApi
|
||||
.expects('search')
|
||||
.once()
|
||||
.withArgs({ page: 1, filters, api: {}, auth: {} })
|
||||
.withArgs({ page: 1, filters, api: { ...initialApiState }, auth: {} })
|
||||
.returns(Promise.resolve({ entities, result }));
|
||||
return loadByCategoryIfNeeded({
|
||||
store,
|
||||
|
@ -77,7 +78,7 @@ describe('searchUtils loadByCategoryIfNeeded()', () => {
|
|||
mockApi
|
||||
.expects('search')
|
||||
.once()
|
||||
.withArgs({ page: 1, filters, api: {}, auth: {} })
|
||||
.withArgs({ page: 1, filters, api: { ...initialApiState }, auth: {} })
|
||||
.returns(Promise.resolve({ entities, result }));
|
||||
return loadByCategoryIfNeeded({
|
||||
store,
|
||||
|
|
|
@ -7,6 +7,7 @@ import UAParser from 'ua-parser-js';
|
|||
|
||||
import { ADDON_TYPE_EXTENSION } from 'core/constants';
|
||||
import { makeI18n } from 'core/i18n/utils';
|
||||
import { initialApiState } from 'core/reducers/api';
|
||||
|
||||
/*
|
||||
* Return a fake authentication token that can be
|
||||
|
@ -109,6 +110,7 @@ export function assertNotHasClass(el, className) {
|
|||
const userAgentForState = 'Mozilla/5.0 (Windows NT 6.1; WOW64; rv:40.0) Gecko/20100101 Firefox/40.1';
|
||||
const { browser, os } = UAParser(userAgentForState);
|
||||
export const signedInApiState = Object.freeze({
|
||||
...initialApiState,
|
||||
lang: 'en-US',
|
||||
token: 'secret-token',
|
||||
userAgent: userAgentForState,
|
||||
|
|
96
yarn.lock
96
yarn.lock
|
@ -107,7 +107,7 @@ ansi-styles@~1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-1.0.0.tgz#cb102df1c56f5123eab8b67cd7b98027a0279178"
|
||||
|
||||
anymatch@^1.3.0:
|
||||
anymatch@^1.1.0, anymatch@^1.3.0:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.0.tgz#a3e52fa39168c825ff57b0248126ce5a8ff95507"
|
||||
dependencies:
|
||||
|
@ -149,6 +149,10 @@ array-equal@^1.0.0:
|
|||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-equal/-/array-equal-1.0.0.tgz#8c2a5ef2472fd9ea742b04c77a75093ba2757c93"
|
||||
|
||||
array-filter@~0.0.0:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-0.0.1.tgz#7da8cf2e26628ed732803581fd21f67cacd2eeec"
|
||||
|
||||
array-find-index@^1.0.1:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1"
|
||||
|
@ -157,6 +161,14 @@ array-flatten@1.1.1:
|
|||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||
|
||||
array-map@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-map/-/array-map-0.0.0.tgz#88a2bab73d1cf7bcd5c1b118a003f66f665fa662"
|
||||
|
||||
array-reduce@~0.0.0:
|
||||
version "0.0.0"
|
||||
resolved "https://registry.yarnpkg.com/array-reduce/-/array-reduce-0.0.0.tgz#173899d3ffd1c7d9383e4479525dbe278cab5f2b"
|
||||
|
||||
array-slice@^0.2.3:
|
||||
version "0.2.3"
|
||||
resolved "https://registry.yarnpkg.com/array-slice/-/array-slice-0.2.3.tgz#dd3cfb80ed7973a75117cdac69b0b99ec86186f5"
|
||||
|
@ -750,7 +762,7 @@ babel-plugin-transform-exponentiation-operator@^6.22.0:
|
|||
babel-plugin-syntax-exponentiation-operator "^6.8.0"
|
||||
babel-runtime "^6.22.0"
|
||||
|
||||
babel-plugin-transform-flow-strip-types@^6.3.13:
|
||||
babel-plugin-transform-flow-strip-types@6.22.0, babel-plugin-transform-flow-strip-types@^6.3.13:
|
||||
version "6.22.0"
|
||||
resolved "https://registry.yarnpkg.com/babel-plugin-transform-flow-strip-types/-/babel-plugin-transform-flow-strip-types-6.22.0.tgz#84cb672935d43714fdc32bce84568d87441cf7cf"
|
||||
dependencies:
|
||||
|
@ -1002,6 +1014,10 @@ block-stream@*:
|
|||
dependencies:
|
||||
inherits "~2.0.0"
|
||||
|
||||
bluebird@^2.9.24:
|
||||
version "2.11.0"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1"
|
||||
|
||||
bluebird@^3.3.0, bluebird@^3.3.1:
|
||||
version "3.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c"
|
||||
|
@ -1143,7 +1159,7 @@ camelcase@^1.0.2:
|
|||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39"
|
||||
|
||||
camelcase@^2.0.0:
|
||||
camelcase@^2.0.0, camelcase@^2.0.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f"
|
||||
|
||||
|
@ -1240,7 +1256,18 @@ cheerio@0.22.0:
|
|||
lodash.reject "^4.4.0"
|
||||
lodash.some "^4.4.0"
|
||||
|
||||
chokidar@^1.0.0, chokidar@^1.4.1, chokidar@^1.5.0, chokidar@^1.6.0:
|
||||
chokidar-cli@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/chokidar-cli/-/chokidar-cli-1.2.0.tgz#8e7f58442273182018be1868e53c22af65a21948"
|
||||
dependencies:
|
||||
anymatch "^1.1.0"
|
||||
bluebird "^2.9.24"
|
||||
chokidar "^1.0.1"
|
||||
lodash "^3.7.0"
|
||||
shell-quote "^1.4.3"
|
||||
yargs "^3.7.2"
|
||||
|
||||
chokidar@^1.0.0, chokidar@^1.0.1, chokidar@^1.4.1, chokidar@^1.5.0, chokidar@^1.6.0:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-1.6.1.tgz#2f4447ab5e96e50fb3d789fd90d4c72e0e4c70c2"
|
||||
dependencies:
|
||||
|
@ -1287,7 +1314,7 @@ cliui@^2.1.0:
|
|||
right-align "^0.1.1"
|
||||
wordwrap "0.0.2"
|
||||
|
||||
cliui@^3.2.0:
|
||||
cliui@^3.0.3, cliui@^3.2.0:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d"
|
||||
dependencies:
|
||||
|
@ -2616,6 +2643,10 @@ flatten@^1.0.2:
|
|||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782"
|
||||
|
||||
flow-bin@0.38.0:
|
||||
version "0.38.0"
|
||||
resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.38.0.tgz#3ae096d401c969cc8b5798253fb82381e2d0237a"
|
||||
|
||||
for-in@^0.1.3:
|
||||
version "0.1.8"
|
||||
resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1"
|
||||
|
@ -3973,7 +4004,7 @@ lodash.uniq@^4.3.0:
|
|||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773"
|
||||
|
||||
lodash@^3.8.0:
|
||||
lodash@^3.7.0, lodash@^3.8.0:
|
||||
version "3.10.1"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6"
|
||||
|
||||
|
@ -4622,10 +4653,6 @@ path-is-inside@^1.0.1:
|
|||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53"
|
||||
|
||||
path-parse@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.5.tgz#3c1adf871ea9cd6c9431b6ea2bd74a0ff055c4c1"
|
||||
|
||||
path-to-regexp@0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
|
@ -5620,16 +5647,10 @@ resolve-from@^2.0.0:
|
|||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-2.0.0.tgz#9480ab20e94ffa1d9e80a804c7ea147611966b57"
|
||||
|
||||
resolve@1.1.x:
|
||||
resolve@1.1.x, resolve@^1.1.6:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
|
||||
|
||||
resolve@^1.1.6:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.3.2.tgz#1f0442c9e0cbb8136e87b9305f932f46c7f28235"
|
||||
dependencies:
|
||||
path-parse "^1.0.5"
|
||||
|
||||
restore-cursor@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541"
|
||||
|
@ -5808,6 +5829,15 @@ shallowequal@^0.2.2:
|
|||
dependencies:
|
||||
lodash.keys "^3.1.2"
|
||||
|
||||
shell-quote@^1.4.3:
|
||||
version "1.6.1"
|
||||
resolved "https://registry.yarnpkg.com/shell-quote/-/shell-quote-1.6.1.tgz#f4781949cce402697127430ea3b3c5476f481767"
|
||||
dependencies:
|
||||
array-filter "~0.0.0"
|
||||
array-map "~0.0.0"
|
||||
array-reduce "~0.0.0"
|
||||
jsonify "~0.0.0"
|
||||
|
||||
shelljs@0.7.7, shelljs@^0.7.5:
|
||||
version "0.7.7"
|
||||
resolved "https://registry.yarnpkg.com/shelljs/-/shelljs-0.7.7.tgz#b2f5c77ef97148f4b4f6e22682e10bba8667cff1"
|
||||
|
@ -6819,6 +6849,10 @@ window-size@0.1.0:
|
|||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
|
||||
|
||||
window-size@^0.1.4:
|
||||
version "0.1.4"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876"
|
||||
|
||||
window-size@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075"
|
||||
|
@ -6883,7 +6917,7 @@ xmlhttprequest-ssl@1.5.3:
|
|||
version "4.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
|
||||
y18n@^3.2.1:
|
||||
y18n@^3.2.0, y18n@^3.2.1:
|
||||
version "3.2.1"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||
|
||||
|
@ -6908,14 +6942,17 @@ yargs@^1.2.6:
|
|||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-1.3.3.tgz#054de8b61f22eefdb7207059eaef9d6b83fb931a"
|
||||
|
||||
yargs@^3.5.4, yargs@~3.10.0:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
|
||||
yargs@^3.5.4, yargs@^3.7.2:
|
||||
version "3.32.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.32.0.tgz#03088e9ebf9e756b69751611d2a5ef591482c995"
|
||||
dependencies:
|
||||
camelcase "^1.0.2"
|
||||
cliui "^2.1.0"
|
||||
decamelize "^1.0.0"
|
||||
window-size "0.1.0"
|
||||
camelcase "^2.0.1"
|
||||
cliui "^3.0.3"
|
||||
decamelize "^1.1.1"
|
||||
os-locale "^1.4.0"
|
||||
string-width "^1.0.1"
|
||||
window-size "^0.1.4"
|
||||
y18n "^3.2.0"
|
||||
|
||||
yargs@^4.7.1:
|
||||
version "4.8.1"
|
||||
|
@ -6954,6 +6991,15 @@ yargs@^6.0.0:
|
|||
y18n "^3.2.1"
|
||||
yargs-parser "^4.2.0"
|
||||
|
||||
yargs@~3.10.0:
|
||||
version "3.10.0"
|
||||
resolved "https://registry.yarnpkg.com/yargs/-/yargs-3.10.0.tgz#f7ee7bd857dd7c1d2d38c0e74efbd681d1431fd1"
|
||||
dependencies:
|
||||
camelcase "^1.0.2"
|
||||
cliui "^2.1.0"
|
||||
decamelize "^1.0.0"
|
||||
window-size "0.1.0"
|
||||
|
||||
yeast@0.1.2:
|
||||
version "0.1.2"
|
||||
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
|
||||
|
|
Загрузка…
Ссылка в новой задаче