[CORL-331] Better tests with types (#2270)

* feat: suspending, banning, now propogation

* feat: new mutation api with hooks support

* feat: better types in tests and refactor

* fix: lint
This commit is contained in:
Kiwi
2019-04-23 21:46:14 +02:00
committed by Wyatt Johnson
parent dbbc1af42e
commit 5150cdf60e
136 changed files with 2546 additions and 2465 deletions
+54 -60
View File
@@ -12117,7 +12117,7 @@
"integrity": "sha1-ETOUSrJHeINHOZVZaIPg05z4hc8=",
"dev": true,
"requires": {
"intl-pluralrules": "github:projectfluent/IntlPluralRules#module"
"intl-pluralrules": "github:projectfluent/IntlPluralRules#94cb0fa1c23ad943bc5aafef43cea132fa51d68b"
}
},
"fluent-langneg": {
@@ -12427,8 +12427,7 @@
"ansi-regex": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz",
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=",
"optional": true
"integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8="
},
"aproba": {
"version": "1.2.0",
@@ -12449,14 +12448,12 @@
"balanced-match": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz",
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=",
"optional": true
"integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c="
},
"brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"optional": true,
"requires": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
@@ -12471,20 +12468,17 @@
"code-point-at": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz",
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=",
"optional": true
"integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c="
},
"concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=",
"optional": true
"integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s="
},
"console-control-strings": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=",
"optional": true
"integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4="
},
"core-util-is": {
"version": "1.0.2",
@@ -12601,8 +12595,7 @@
"inherits": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz",
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=",
"optional": true
"integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4="
},
"ini": {
"version": "1.3.5",
@@ -12614,7 +12607,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz",
"integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=",
"optional": true,
"requires": {
"number-is-nan": "^1.0.0"
}
@@ -12629,7 +12621,6 @@
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz",
"integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==",
"optional": true,
"requires": {
"brace-expansion": "^1.1.7"
}
@@ -12637,14 +12628,12 @@
"minimist": {
"version": "0.0.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz",
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=",
"optional": true
"integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0="
},
"minipass": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz",
"integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==",
"optional": true,
"requires": {
"safe-buffer": "^5.1.1",
"yallist": "^3.0.0"
@@ -12663,7 +12652,6 @@
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz",
"integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=",
"optional": true,
"requires": {
"minimist": "0.0.8"
}
@@ -12744,8 +12732,7 @@
"number-is-nan": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz",
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=",
"optional": true
"integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0="
},
"object-assign": {
"version": "4.1.1",
@@ -12757,7 +12744,6 @@
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
"integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=",
"optional": true,
"requires": {
"wrappy": "1"
}
@@ -12843,8 +12829,7 @@
"safe-buffer": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz",
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==",
"optional": true
"integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg=="
},
"safer-buffer": {
"version": "2.1.2",
@@ -12880,7 +12865,6 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz",
"integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=",
"optional": true,
"requires": {
"code-point-at": "^1.0.0",
"is-fullwidth-code-point": "^1.0.0",
@@ -12900,7 +12884,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz",
"integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=",
"optional": true,
"requires": {
"ansi-regex": "^2.0.0"
}
@@ -12944,14 +12927,12 @@
"wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=",
"optional": true
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8="
},
"yallist": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz",
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=",
"optional": true
"integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k="
}
}
},
@@ -13449,9 +13430,9 @@
}
},
"graphql-schema-typescript": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/graphql-schema-typescript/-/graphql-schema-typescript-1.2.1.tgz",
"integrity": "sha512-ipZh3Epm/Kqcy6MF5FM6uxwCMFok07q+6qyxFOa7ViRufcjzH9Y3nECmECH5WgqRGl2wR6TmskbZd5qJJrGpoA==",
"version": "1.2.9",
"resolved": "https://registry.npmjs.org/graphql-schema-typescript/-/graphql-schema-typescript-1.2.9.tgz",
"integrity": "sha512-IGbQW8aC7MfXqJuaTZ0zHoqLGHUPHX+skV6qf0buqCbumqKyQ+cy8vqUqQksMr8Y/Ga++sB8cvMSQ6yeUbF6rQ==",
"dev": true,
"requires": {
"yargs": "^11.0.0"
@@ -23278,9 +23259,9 @@
"dev": true
},
"prettier": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.13.7.tgz",
"integrity": "sha512-KIU72UmYPGk4MujZGYMFwinB7lOf2LsDNGSOC8ufevsrPLISrZbNJlWstRi3m0AMuszbH+EFSQ/r6w56RSPK6w==",
"version": "1.16.4",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.16.4.tgz",
"integrity": "sha512-ZzWuos7TI5CKUeQAtFd6Zhm2s6EpAD/ZLApIhsF9pRvRtM1RFo61dM/4MSRUA0SuLugA/zgrZD8m0BaY46Og7g==",
"dev": true
},
"pretty-error": {
@@ -27849,9 +27830,9 @@
"dev": true
},
"tslint": {
"version": "5.11.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.11.0.tgz",
"integrity": "sha1-mPMMAurjzecAYgHkwzywi0hYHu0=",
"version": "5.15.0",
"resolved": "https://registry.npmjs.org/tslint/-/tslint-5.15.0.tgz",
"integrity": "sha512-6bIEujKR21/3nyeoX2uBnE8s+tMXCQXhqMmaIPJpHmXJoBJPTLcI7/VHRtUwMhnLVdwLqqY3zmd8Dxqa5CVdJA==",
"dev": true,
"requires": {
"babel-code-frame": "^6.22.0",
@@ -27860,18 +27841,19 @@
"commander": "^2.12.1",
"diff": "^3.2.0",
"glob": "^7.1.1",
"js-yaml": "^3.7.0",
"js-yaml": "^3.13.0",
"minimatch": "^3.0.4",
"mkdirp": "^0.5.1",
"resolve": "^1.3.2",
"semver": "^5.3.0",
"tslib": "^1.8.0",
"tsutils": "^2.27.2"
"tsutils": "^2.29.0"
},
"dependencies": {
"glob": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz",
"integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==",
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.3.tgz",
"integrity": "sha512-vcfuiIxogLV4DlGBHIUOwI0IbrJ8HWPc4MU7HzviGeNho/UJDfi6B5p3sHeWIQ0KGIU0Jpxi5ZHxemQfLkkAwQ==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
@@ -27882,21 +27864,22 @@
"path-is-absolute": "^1.0.0"
}
},
"tsutils": {
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"js-yaml": {
"version": "3.13.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
"integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
}
}
},
"tslint-config-prettier": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.17.0.tgz",
"integrity": "sha512-NKWNkThwqE4Snn4Cm6SZB7lV5RMDDFsBwz6fWUkTxOKGjMx8ycOHnjIbhn7dZd5XmssW3CwqUjlANR6EhP9YQw==",
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/tslint-config-prettier/-/tslint-config-prettier-1.18.0.tgz",
"integrity": "sha512-xPw9PgNPLG3iKRxmK7DWr+Ea/SzrvfHtjFt5LBl61gk2UBG/DB9kCXRjv+xyIU1rUtnayLeMUVJBcMX8Z17nDg==",
"dev": true
},
"tslint-loader": {
@@ -27924,18 +27907,29 @@
}
},
"tslint-react": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/tslint-react/-/tslint-react-3.6.0.tgz",
"integrity": "sha512-AIv1QcsSnj7e9pFir6cJ6vIncTqxfqeFF3Lzh8SuuBljueYzEAtByuB6zMaD27BL0xhMEqsZ9s5eHuCONydjBw==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/tslint-react/-/tslint-react-4.0.0.tgz",
"integrity": "sha512-9fNE0fm9zNDx1+b6hgy8rgDN2WsQLRiIrn3+fbqm0tazBVF6jiaCFAITxmU+WSFWYE03Xhp1joCircXOe1WVAQ==",
"dev": true,
"requires": {
"tsutils": "^2.13.1"
"tsutils": "^3.9.1"
},
"dependencies": {
"tsutils": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.10.0.tgz",
"integrity": "sha512-q20XSMq7jutbGB8luhKKsQldRKWvyBO2BGqni3p4yq8Ys9bEP/xQw3KepKmMRt9gJ4lvQSScrihJrcKdKoSU7Q==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
}
}
}
},
"tsutils": {
"version": "2.27.1",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.27.1.tgz",
"integrity": "sha512-AE/7uzp32MmaHvNNFES85hhUDHFdFZp6OAiZcd6y4ZKKIg6orJTm8keYWBhIhrJQH3a4LzNKat7ZPXZt5aTf6w==",
"version": "2.29.0",
"resolved": "https://registry.npmjs.org/tsutils/-/tsutils-2.29.0.tgz",
"integrity": "sha512-g5JVHCIJwzfISaXpXE1qvNalca5Jwob6FjI4AoPlqMusJ6ftFE7IkkFoMhVLRgK+4Kx3gkzb8UZK5t5yTTvEmA==",
"dev": true,
"requires": {
"tslib": "^1.8.1"
+5 -5
View File
@@ -230,7 +230,7 @@
"fluent-langneg": "^0.1.0",
"fluent-react": "^0.8.3",
"graphql-schema-linter": "^0.2.0",
"graphql-schema-typescript": "^1.2.1",
"graphql-schema-typescript": "^1.2.9",
"gulp": "^4.0.0",
"gulp-babel": "^8.0.0",
"gulp-cli": "^2.0.1",
@@ -260,7 +260,7 @@
"postcss-nested": "^4.1.1",
"postcss-prepend-imports": "^1.0.1",
"postcss-preset-env": "^6.5.0",
"prettier": "^1.13.7",
"prettier": "^1.16.4",
"prop-types": "^15.6.2",
"pstree.remy": "^1.1.0",
"pym.js": "^1.3.2",
@@ -294,11 +294,11 @@
"ts-node": "^6.2.0",
"tsconfig-paths": "^3.4.2",
"tsconfig-paths-webpack-plugin": "^3.1.4",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.17.0",
"tslint": "^5.15.0",
"tslint-config-prettier": "^1.18.0",
"tslint-loader": "^3.6.0",
"tslint-plugin-prettier": "^2.0.1",
"tslint-react": "^3.6.0",
"tslint-react": "^4.0.0",
"typed-css-modules": "^0.3.4",
"typeface-manuale": "0.0.54",
"typeface-source-sans-pro": "0.0.54",
+8 -16
View File
@@ -4,16 +4,6 @@ const { getGraphQLConfig } = require("graphql-config");
const path = require("path");
const fs = require("fs");
function lintAndWrite(files) {
const linter = new Linter({ fix: true });
for (const { fileName, types } of files) {
const configuration = Configuration.findConfiguration(null, fileName)
.results;
linter.lint(fileName, types, configuration);
}
}
async function main() {
const config = getGraphQLConfig(__dirname);
const projects = config.getProjects();
@@ -28,8 +18,8 @@ async function main() {
config: {
contextType: "TenantContext",
importStatements: [
'import { Cursor } from "talk-server/models/helpers/connection";',
'import TenantContext from "talk-server/graph/tenant/context";',
'import { Cursor } from "talk-server/models/helpers/connection";',
],
customScalarType: { Cursor: "Cursor", Time: "Date" },
},
@@ -40,7 +30,10 @@ async function main() {
__dirname,
"../src/core/client/framework/schema/__generated__/types.ts"
),
config: {},
config: {
smartTResult: true,
smartTParent: true,
},
},
];
@@ -55,16 +48,15 @@ async function main() {
}
// Create the types for this file.
file.types = await generateTSTypesAsString(schema, {
const types = await generateTSTypesAsString(schema, {
tabSpaces: 2,
typePrefix: "GQL",
strictNulls: false,
...file.config,
});
}
// Send the files off to the linter to be linted and written.
lintAndWrite(files);
fs.writeFileSync(file.fileName, types);
}
return files;
}
@@ -22,42 +22,30 @@ const UserStatusChangeContainer: StatelessComponent<Props> = props => {
const banUser = useMutation(BanUserMutation);
const removeUserBan = useMutation(RemoveUserBanMutation);
const [showBanned, setShowBanned] = useState<boolean>(false);
const handleBan = useCallback(
() => {
if (props.user.status.ban.active) {
return;
}
setShowBanned(true);
},
[props.user, setShowBanned]
);
const handleRemoveBan = useCallback(
() => {
if (!props.user.status.ban.active) {
return;
}
removeUserBan({ userID: props.user.id });
},
[props.user, removeUserBan]
);
const handleSuspend = useCallback(
() => {
if (props.user.status.suspension.active) {
return;
}
// TODO: (cvle)
},
[props.user]
);
const handleRemoveSuspension = useCallback(
() => {
if (!props.user.status.suspension.active) {
return;
}
// TODO: (cvle)
},
[props.user]
);
const handleBan = useCallback(() => {
if (props.user.status.ban.active) {
return;
}
setShowBanned(true);
}, [props.user, setShowBanned]);
const handleRemoveBan = useCallback(() => {
if (!props.user.status.ban.active) {
return;
}
removeUserBan({ userID: props.user.id });
}, [props.user, removeUserBan]);
const handleSuspend = useCallback(() => {
if (props.user.status.suspension.active) {
return;
}
// TODO: (cvle)
}, [props.user]);
const handleRemoveSuspension = useCallback(() => {
if (!props.user.status.suspension.active) {
return;
}
// TODO: (cvle)
}, [props.user]);
if (props.user.role !== GQLUSER_ROLE.COMMENTER) {
return (
@@ -23,8 +23,8 @@ class NavigationWarningContainer extends React.Component<Props> {
"You have unsaved input. Are you sure you want to leave this page?"
);
this.removeTransitionHook = props.router.addTransitionHook(
() => (this.props.active ? warningMessage : true)
this.removeTransitionHook = props.router.addTransitionHook(() =>
this.props.active ? warningMessage : true
);
}
@@ -48,12 +48,11 @@ const CustomCSSConfig: StatelessComponent<Props> = ({ disabled }) => (
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -49,12 +49,11 @@ const PermittedDomainsConfig: StatelessComponent<Props> = ({ disabled }) => (
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -1,10 +1,10 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { AdvancedConfigContainer_settings as SettingsData } from "talk-admin/__generated__/AdvancedConfigContainer_settings.graphql";
import { pureMerge } from "talk-common/utils";
import { withFragmentContainer } from "talk-framework/lib/relay";
import AdvancedConfig from "../components/AdvancedConfig";
@@ -28,7 +28,7 @@ class AdvancedConfigContainer extends React.Component<Props> {
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
this.initialValues = pureMerge(this.initialValues, values);
};
public render() {
@@ -36,12 +36,11 @@ const ClientSecretField: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -41,12 +41,11 @@ const ClientSecretField: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -55,7 +55,8 @@ const FacebookConfig: StatelessComponent<Props> = ({
>
<Typography>
To enable the integration with Facebook Authentication, you need to
create and set up a web application. For more information visit:<br />
create and set up a web application. For more information visit:
<br />
{"https://developers.facebook.com/docs/facebook-login/web"}
</Typography>
</Localized>
@@ -53,7 +53,8 @@ const GoogleConfig: StatelessComponent<Props> = ({ disabled, callbackURL }) => (
>
<Typography>
To enable the integration with Google Authentication you need to
create and set up a web application. For more information visit:<br />
create and set up a web application. For more information visit:
<br />
{
"https://developers.google.com/identity/protocols/OAuth2WebServer#creatingcred"
}
@@ -106,12 +106,11 @@ const OIDCConfig: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -168,12 +167,11 @@ const OIDCConfig: StatelessComponent<Props> = ({
Discover
</Button>
</Flex>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -201,12 +199,11 @@ const OIDCConfig: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -234,12 +231,11 @@ const OIDCConfig: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -267,12 +263,11 @@ const OIDCConfig: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -1,11 +1,12 @@
import { FORM_ERROR, FormApi } from "final-form";
import { Localized } from "fluent-react/compat";
import { RouteProps } from "found";
import { get, merge } from "lodash";
import { get } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { AuthConfigContainer_auth as AuthData } from "talk-admin/__generated__/AuthConfigContainer_auth.graphql";
import { pureMerge } from "talk-common/utils";
import { TalkContext, withContext } from "talk-framework/lib/bootstrap";
import {
AddSubmitHook,
@@ -80,7 +81,7 @@ class AuthConfigContainer extends React.Component<Props> {
};
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
this.initialValues = pureMerge(this.initialValues, values);
};
public render() {
@@ -46,12 +46,11 @@ const ClosedStreamMessageConfig: StatelessComponent<Props> = ({ disabled }) => (
value={input.value}
/>
</Suspense>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -72,12 +72,11 @@ const ClosingCommentStreamsConfig: StatelessComponent<Props> = ({
value={input.value}
disabled={disabled}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -63,12 +63,11 @@ const CommentEditingConfig: StatelessComponent<Props> = ({ disabled }) => (
value={input.value}
disabled={disabled}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -97,12 +97,11 @@ const CommentLengthConfig: StatelessComponent<Props> = ({ disabled }) => (
textAlignCenter
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -148,12 +147,11 @@ const CommentLengthConfig: StatelessComponent<Props> = ({ disabled }) => (
textAlignCenter
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -68,12 +68,11 @@ const GuidelinesConfig: StatelessComponent<Props> = ({ disabled }) => (
value={input.value}
/>
</Suspense>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -81,12 +81,11 @@ const SitewideCommentingConfig: StatelessComponent<Props> = ({ disabled }) => (
value={input.value}
/>
</Suspense>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -1,10 +1,10 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { GeneralConfigContainer_settings as SettingsData } from "talk-admin/__generated__/GeneralConfigContainer_settings.graphql";
import { pureMerge } from "talk-common/utils";
import { withFragmentContainer } from "talk-framework/lib/relay";
import GeneralConfig from "../components/GeneralConfig";
@@ -28,7 +28,7 @@ class GeneralConfigContainer extends React.Component<Props> {
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
this.initialValues = pureMerge(this.initialValues, values);
};
public render() {
@@ -39,12 +39,11 @@ const APIKeyField: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -107,12 +107,11 @@ const AkismetConfig: StatelessComponent<Props> = ({ disabled }) => {
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -116,12 +116,11 @@ const PerspectiveConfig: StatelessComponent<Props> = ({ disabled }) => {
placeholder={TOXICITY_DEFAULT.toString()}
textAlignCenter
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -197,12 +196,11 @@ const PerspectiveConfig: StatelessComponent<Props> = ({ disabled }) => {
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -1,10 +1,10 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { ModerationConfigContainer_settings as SettingsData } from "talk-admin/__generated__/ModerationConfigContainer_settings.graphql";
import { pureMerge } from "talk-common/utils";
import { withFragmentContainer } from "talk-framework/lib/relay";
import ModerationConfig from "../components/ModerationConfig";
@@ -28,7 +28,7 @@ class ModerationConfigContainer extends React.Component<Props> {
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
this.initialValues = pureMerge(this.initialValues, values);
};
public render() {
@@ -50,12 +50,11 @@ const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -53,12 +53,11 @@ const OrganizationNameConfig: StatelessComponent<Props> = ({ disabled }) => (
spellCheck={false}
fullWidth
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -1,10 +1,10 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { OrganizationConfigContainer_settings as SettingsData } from "talk-admin/__generated__/OrganizationConfigContainer_settings.graphql";
import { pureMerge } from "talk-common/utils";
import { withFragmentContainer } from "talk-framework/lib/relay";
import OrganizationConfig from "../components/OrganizationConfig";
@@ -28,7 +28,7 @@ class OrganizationConfigContainer extends React.Component<Props> {
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
this.initialValues = pureMerge(this.initialValues, values);
};
public render() {
@@ -47,12 +47,11 @@ const WordListTextArea: StatelessComponent<Props> = ({
autoCapitalize="off"
spellCheck={false}
/>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</>
)}
</Field>
@@ -1,10 +1,10 @@
import { FormApi } from "final-form";
import { RouteProps } from "found";
import { merge } from "lodash";
import React from "react";
import { graphql } from "react-relay";
import { WordListConfigContainer_settings as SettingsData } from "talk-admin/__generated__/WordListConfigContainer_settings.graphql";
import { pureMerge } from "talk-common/utils";
import { withFragmentContainer } from "talk-framework/lib/relay";
import WordListConfig from "../components/WordListConfig";
@@ -28,7 +28,7 @@ class WordListConfigContainer extends React.Component<Props> {
}
private handleOnInitValues = (values: any) => {
this.initialValues = merge({}, this.initialValues, values);
this.initialValues = pureMerge(this.initialValues, values);
};
public render() {
@@ -44,12 +44,11 @@ const EmailField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -47,12 +47,11 @@ const ConfirmEmailField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -44,12 +44,11 @@ const EmailField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -53,12 +53,11 @@ const SetPasswordField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -51,12 +51,11 @@ const CreateUsernameField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -64,12 +64,11 @@ const SignInWithEmail: StatelessComponent<SignInWithEmailForm> = props => {
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -76,13 +76,12 @@ function markPhrasesHTML(
return text;
}
return tokens
.map(
(token, i) =>
// Using our Regexp patterns it returns tokens arranged this way
// [STRING_WITH_NO_MATCH, NEW_WORD_DELIMITER, MATCHED_WORD, ...].
// This pattern repeats throughout. Next line will mark MATCHED_WORD
// and escape all tokens.
i % 3 === 2 ? `<mark>${escapeHTML(token)}</mark>` : escapeHTML(token)
.map((token, i) =>
// Using our Regexp patterns it returns tokens arranged this way
// [STRING_WITH_NO_MATCH, NEW_WORD_DELIMITER, MATCHED_WORD, ...].
// This pattern repeats throughout. Next line will mark MATCHED_WORD
// and escape all tokens.
i % 3 === 2 ? `<mark>${escapeHTML(token)}</mark>` : escapeHTML(token)
)
.join("");
}
@@ -1,8 +1,9 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createAccessToken,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -12,39 +13,37 @@ import {
import create from "../create";
import { emptyModerationQueues, settings, users } from "../fixtures";
const viewer = users.admins[0];
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/login");
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
moderationQueues: sinon.stub().returns(emptyModerationQueues),
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
viewer: sinon
.stub()
.returns(
merge(
{ ...users.admins[0], email: "", username: "", profiles: [] },
get(customResolver, "Query.viewer")
)
),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () =>
pureMerge<typeof viewer>(viewer, {
email: "",
username: "",
profiles: [],
}),
moderationQueues: () => emptyModerationQueues,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("SIGN_IN", "authView");
localRecord.setValue(true, "loggedIn");
localRecord.setValue(createAccessToken(), "accessToken");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
@@ -62,44 +61,60 @@ it("renders addEmailAddress view", async () => {
it("renders createUsername view", async () => {
const { root } = await createTestRenderer({
Query: {
viewer: {
email: "hans@test.com",
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () =>
pureMerge<typeof viewer>(viewer, {
email: "hans@test.com",
username: "",
profiles: [],
}),
},
},
}),
});
await waitForElement(() => within(root).queryByText("Create Username"));
});
it("renders createPassword view", async () => {
const { root } = await createTestRenderer({
Query: {
viewer: {
email: "hans@test.com",
username: "hans",
resolvers: createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
moderationQueues: () => emptyModerationQueues,
viewer: () =>
pureMerge<typeof viewer>(viewer, {
email: "hans@test.com",
username: "hans",
profiles: [],
}),
},
},
}),
});
await waitForElement(() => within(root).queryByText("Create Password"));
});
it("do not render createPassword view when local auth is disabled", async () => {
await createTestRenderer({
Query: {
viewer: {
email: "hans@test.com",
username: "hans",
},
settings: {
auth: {
integrations: {
local: {
enabled: false,
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () =>
pureMerge<typeof viewer>(viewer, {
email: "hans@test.com",
username: "hans",
profiles: [],
}),
settings: () =>
pureMerge<typeof settings>(settings, {
auth: {
integrations: {
local: {
enabled: false,
},
},
},
},
},
}),
},
},
}),
});
await wait(() =>
@@ -111,13 +126,16 @@ it("do not render createPassword view when local auth is disabled", async () =>
it("complete account", async () => {
await createTestRenderer({
Query: {
viewer: {
email: "hans@test.com",
username: "hans",
profiles: [{ __typename: "LocalProfile" }],
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () =>
pureMerge<typeof viewer>(viewer, {
email: "hans@test.com",
username: "hans",
profiles: [{ __typename: "LocalProfile" }],
}),
},
},
}),
});
await wait(() =>
expect(window.location.toString()).toBe(
@@ -1,8 +1,9 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createAccessToken,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
wait,
@@ -13,33 +14,37 @@ import {
import create from "../create";
import { settings, users } from "../fixtures";
const viewer = users.admins[0];
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/login");
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
viewer: sinon.stub().returns({ ...users.admins[0], email: "" }),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () =>
pureMerge<typeof viewer>(viewer, {
email: "",
}),
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("ADD_EMAIL_ADDRESS", "authView");
localRecord.setValue(true, "loggedIn");
localRecord.setValue(createAccessToken(), "accessToken");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("completeAccountBox")
);
@@ -94,21 +99,22 @@ it("accepts valid email", async () => {
it("shows server error", async () => {
const email = "hans@test.com";
const setEmail = sinon.stub().callsFake((_: any, data: any) => {
throw new Error("server error");
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
setEmail: () => {
throw new Error("server error");
},
},
});
const {
form,
emailAddressField,
confirmEmailAddressField,
} = await createTestRenderer(
{
Mutation: {
setEmail,
},
},
{ muteNetworkErrors: true }
);
} = await createTestRenderer({
resolvers,
muteNetworkErrors: true,
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
@@ -130,27 +136,28 @@ it("shows server error", async () => {
it("successfully sets email", async () => {
const email = "hans@test.com";
const setEmail = sinon.stub().callsFake((_: any, data: any) => {
expectAndFail(data.input).toEqual({
email,
clientMutationId: data.input.clientMutationId,
});
return {
user: {
id: "me",
email,
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
setEmail: ({ variables }) => {
expectAndFail(variables).toEqual({
email,
});
return {
user: {
id: "me",
email,
},
};
},
clientMutationId: data.input.clientMutationId,
};
},
});
const {
form,
emailAddressField,
confirmEmailAddressField,
} = await createTestRenderer({
Mutation: {
setEmail,
},
resolvers,
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
@@ -169,5 +176,5 @@ it("successfully sets email", async () => {
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
expect(setEmail.called).toBe(true);
expect(resolvers.Mutation!.setEmail!.called).toBe(true);
});
@@ -1,8 +1,9 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createAccessToken,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
wait,
@@ -13,33 +14,37 @@ import {
import create from "../create";
import { settings, users } from "../fixtures";
const viewer = users.admins[0];
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/login");
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
viewer: sinon.stub().returns({ ...users.admins[0], profiles: [] }),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () =>
pureMerge<typeof viewer>(viewer, {
profiles: [],
}),
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("CREATE_PASSWORD", "authView");
localRecord.setValue(true, "loggedIn");
localRecord.setValue(createAccessToken(), "accessToken");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("completeAccountBox")
);
@@ -76,17 +81,18 @@ it("checks for invalid password", async () => {
it("shows server error", async () => {
const password = "secretpassword";
const setPassword = sinon.stub().callsFake((_: any, data: any) => {
throw new Error("server error");
});
const { form, passwordField } = await createTestRenderer(
{
Mutation: {
setPassword,
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
setPassword: () => {
throw new Error("server error");
},
},
{ muteNetworkErrors: true }
);
});
const { form, passwordField } = await createTestRenderer({
resolvers,
muteNetworkErrors: true,
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
@@ -104,23 +110,23 @@ it("shows server error", async () => {
it("successfully sets password", async () => {
const password = "secretpassword";
const setPassword = sinon.stub().callsFake((_: any, data: any) => {
expectAndFail(data.input).toEqual({
password,
clientMutationId: data.input.clientMutationId,
});
return {
user: {
id: "me",
profiles: [],
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
setPassword: ({ variables }) => {
expectAndFail(variables).toEqual({
password,
});
return {
user: {
id: "me",
profiles: [],
},
};
},
clientMutationId: data.input.clientMutationId,
};
},
});
const { form, passwordField } = await createTestRenderer({
Mutation: {
setPassword,
},
resolvers,
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
@@ -135,5 +141,5 @@ it("successfully sets password", async () => {
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
expect(setPassword.called).toBe(true);
expect(resolvers.Mutation!.setPassword!.called).toBe(true);
});
@@ -1,8 +1,9 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createAccessToken,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
wait,
@@ -14,29 +15,27 @@ import create from "../create";
import { settings } from "../fixtures";
async function createTestRenderer(
customResolver: any = {},
options: { muteNetworkErrors?: boolean; logNetwork?: boolean } = {}
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/login");
const resolvers = {
...customResolver,
Query: {
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
},
};
const { testRenderer, context } = create({
// Set this to true, to see graphql responses.
logNetwork: options.logNetwork,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("CREATE_USERNAME", "authView");
localRecord.setValue(true, "loggedIn");
localRecord.setValue(createAccessToken(), "accessToken");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
@@ -75,17 +74,17 @@ it("checks for invalid username", async () => {
it("shows server error", async () => {
const username = "hans";
const setUsername = sinon.stub().callsFake((_: any, data: any) => {
throw new Error("server error");
});
const { form, usernameField } = await createTestRenderer(
{
Mutation: {
setUsername,
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
setUsername: () => {
throw new Error("server error");
},
},
{ muteNetworkErrors: true }
);
});
const { form, usernameField } = await createTestRenderer({
resolvers,
muteNetworkErrors: true,
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
@@ -103,24 +102,25 @@ it("shows server error", async () => {
it("successfully sets username", async () => {
const username = "hans";
const setUsername = sinon.stub().callsFake((_: any, data: any) => {
expectAndFail(data.input).toEqual({
username,
clientMutationId: data.input.clientMutationId,
});
return {
user: {
id: "me",
username,
},
clientMutationId: data.input.clientMutationId,
};
});
const { form, usernameField } = await createTestRenderer({
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
setUsername,
setUsername: ({ variables }) => {
expectAndFail(variables).toEqual({
username,
});
return {
user: {
id: "me",
username,
},
};
},
},
});
const { form, usernameField } = await createTestRenderer({
resolvers,
});
const submitButton = form.find(
i => i.type === "button" && i.props.type === "submit"
);
@@ -134,5 +134,5 @@ it("successfully sets username", async () => {
await wait(() => expect(submitButton.props.disabled).toBe(false));
expect(toJSON(form)).toMatchSnapshot();
expect(setUsername.called).toBe(true);
expect(resolvers.Mutation!.setUsername!.called).toBe(true);
});
@@ -1,10 +1,17 @@
import sinon from "sinon";
import {
GQLResolver,
QueryToModerationQueuesResolver,
} from "talk-framework/schema";
import {
createAccessToken,
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
} from "talk-framework/testHelpers";
import { pureMerge } from "talk-common/utils";
import create from "../create";
import {
emptyModerationQueues,
@@ -13,26 +20,41 @@ import {
users,
} from "../fixtures";
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const viewer = users.admins[0];
it("redirect when already logged in", async () => {
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/login");
create({
resolvers,
// Set this to true, to see graphql responses.
logNetwork: false,
initLocalState: localRecord => {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
moderationQueues: createQueryResolverStub<
QueryToModerationQueuesResolver
>(() => emptyModerationQueues),
comments: () => emptyRejectedComments,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
localRecord.setValue(createAccessToken(), "accessToken");
localRecord.setValue("SIGN_IN", "authView");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context };
}
it("redirect when already logged in", async () => {
await createTestRenderer();
await wait(() =>
expect(window.location.toString()).toBe(
"http://localhost/admin/moderate/reported"
@@ -41,14 +63,8 @@ it("redirect when already logged in", async () => {
});
it("redirect to redirectPath when already logged in", async () => {
replaceHistoryLocation("http://localhost/admin/login");
create({
resolvers,
// Set this to true, to see graphql responses.
logNetwork: false,
await createTestRenderer({
initLocalState: localRecord => {
localRecord.setValue(true, "loggedIn");
localRecord.setValue(createAccessToken(), "accessToken");
localRecord.setValue("/admin/moderate/pending", "redirectPath");
},
});
@@ -1,9 +1,16 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { replaceHistoryLocation, wait } from "talk-framework/testHelpers";
import { pureMerge } from "talk-common/utils";
import { LOCAL_ID, lookup } from "talk-framework/lib/relay";
import {
GQLResolver,
QueryToModerationQueuesResolver,
} from "talk-framework/schema";
import {
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
} from "talk-framework/testHelpers";
import create from "../create";
import {
@@ -12,39 +19,41 @@ import {
settings,
} from "../fixtures";
function createTestRenderer(): {
testRenderer: ReactTestRenderer;
context: TalkContext;
} {
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/moderate");
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
},
};
const { testRenderer, context } = create({
resolvers,
// Set this to true, to see graphql responses.
logNetwork: false,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
moderationQueues: createQueryResolverStub<
QueryToModerationQueuesResolver
>(() => emptyModerationQueues),
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(false, "loggedIn");
localRecord.setValue("SIGN_IN", "authView");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context };
}
it("redirect when not logged in", async () => {
const { context } = createTestRenderer();
const { context } = await createTestRenderer();
await wait(() => {
expect(
context.relayEnvironment
.getStore()
.getSource()
.get(LOCAL_ID)!.redirectPath
).toBe("/admin/moderate/reported");
expect(lookup(context.relayEnvironment, LOCAL_ID)!.redirectPath).toBe(
"/admin/moderate/reported"
);
expect(window.location.toString()).toBe("http://localhost/admin/login");
});
});
@@ -1,11 +1,12 @@
import { ReactTestRenderer } from "react-test-renderer";
import sinon from "sinon";
import { TalkContext } from "talk-framework/lib/bootstrap";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { GQLUSER_ROLE } from "talk-framework/schema";
import { pureMerge } from "talk-common/utils";
import { LOCAL_ID, lookup } from "talk-framework/lib/relay";
import { GQLResolver, GQLUSER_ROLE } from "talk-framework/schema";
import {
createAccessToken,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -20,29 +21,32 @@ import {
users,
} from "../fixtures";
function createTestRenderer(
userDiff: any = {}
): {
testRenderer: ReactTestRenderer;
context: TalkContext;
} {
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/moderate/reported");
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns({ ...users.admins[0], ...userDiff }),
},
};
const { testRenderer, context } = create({
resolvers,
// Set this to true, to see graphql responses.
logNetwork: false,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
localRecord.setValue(createAccessToken(), "accessToken");
localRecord.setValue("SIGN_IN", "authView");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context };
@@ -51,7 +55,16 @@ function createTestRenderer(
it("show restricted screen for commenters and staff", async () => {
const restrictedRoles = [GQLUSER_ROLE.COMMENTER, GQLUSER_ROLE.STAFF];
for (const role of restrictedRoles) {
const { testRenderer } = createTestRenderer({ role });
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () =>
pureMerge<typeof viewer>(viewer, {
role,
}),
},
}),
});
const authBox = await waitForElement(() =>
within(testRenderer.root).getByTestID("authBox")
);
@@ -60,8 +73,15 @@ it("show restricted screen for commenters and staff", async () => {
});
it("sign out when clicking on sign in as", async () => {
const { context, testRenderer } = createTestRenderer({
role: GQLUSER_ROLE.COMMENTER,
const { testRenderer, context } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () =>
pureMerge<typeof viewer>(viewer, {
role: GQLUSER_ROLE.COMMENTER,
}),
},
}),
});
const authBox = await waitForElement(() =>
within(testRenderer.root).getByTestID("authBox")
@@ -81,18 +101,10 @@ it("sign out when clicking on sign in as", async () => {
.props.onClick();
await wait(() => {
expect(
context.relayEnvironment
.getStore()
.getSource()
.get(LOCAL_ID)!.redirectPath
).toBe("/admin/moderate/reported");
expect(lookup(context.relayEnvironment, LOCAL_ID)!.redirectPath).toBe(
"/admin/moderate/reported"
);
});
expect(
context.relayEnvironment
.getStore()
.getSource()
.get(LOCAL_ID)!.loggedIn
).toBeFalsy();
expect(lookup(context.relayEnvironment, LOCAL_ID)!.loggedIn).toBeFalsy();
});
@@ -1,8 +1,11 @@
import { ReactTestInstance } from "react-test-renderer";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createAccessToken,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -16,32 +19,34 @@ import {
settings,
} from "../fixtures";
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
},
};
const inputPredicate = (name: string) => (n: ReactTestInstance) => {
return n.props.name === name && n.props.onChange;
};
async function createTestRenderer() {
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
// deliberately setting to a different route,
// it should be smart enough to reroute to /admin/login.
replaceHistoryLocation("http://localhost/admin/moderate");
const { testRenderer, context } = create({
resolvers,
// Set this to true, to see graphql responses.
logNetwork: false,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(false, "loggedIn");
localRecord.setValue("SIGN_IN", "authView");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const form = await waitForElement(() =>
within(testRenderer.root).getByType("form")
);
@@ -121,11 +126,11 @@ it("shows server error", async () => {
it("submits form successfully", async () => {
const { form, context } = await createTestRenderer();
form
.find(inputPredicate("email"))
within(form)
.getByLabelText("Email Address")
.props.onChange({ target: { value: "hans@test.com" } });
form
.find(inputPredicate("password"))
within(form)
.getByLabelText("Password")
.props.onChange({ target: { value: "testtest" } });
const accessToken = createAccessToken();
@@ -1,7 +1,11 @@
import sinon from "sinon";
import { LOCAL_ID } from "talk-framework/lib/relay";
import { pureMerge } from "talk-common/utils";
import { LOCAL_ID, lookup } from "talk-framework/lib/relay";
import { GQLResolver } from "talk-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -16,26 +20,39 @@ import {
users,
} from "../fixtures";
const resolvers = {
Query: {
settings: sinon.stub().returns(settings),
moderationQueues: sinon.stub().returns(emptyModerationQueues),
comments: sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
replaceHistoryLocation("http://localhost/admin/moderate/reported");
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return { testRenderer, context };
}
it("logs out", async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
const { testRenderer, context } = create({
resolvers,
// Set this to true, to see graphql responses.
logNetwork: false,
initLocalState: localRecord => {
localRecord.setValue(true, "loggedIn");
},
});
const { testRenderer, context } = await createTestRenderer();
const restMock = sinon.mock(context.rest);
restMock
@@ -60,11 +77,6 @@ it("logs out", async () => {
signOutButton.props.onClick();
await wait(() => {
expect(
context.relayEnvironment
.getStore()
.getSource()
.get(LOCAL_ID)!.loggedIn
).toBeFalsy();
expect(lookup(context.relayEnvironment, LOCAL_ID)!.loggedIn).toBeFalsy();
});
});
@@ -1,16 +1,16 @@
import { merge } from "lodash";
import TestRenderer from "react-test-renderer";
import { pureMerge } from "talk-common/utils";
import {
GQLResolver,
GQLUSER_ROLE,
GQLUSER_STATUS,
QueryToSettingsResolver,
QueryToUsersResolver,
QueryToViewerResolver,
} from "talk-framework/schema";
import {
createMutationResolverStub,
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
findParentWithType,
replaceHistoryLocation,
waitForElement,
@@ -18,11 +18,6 @@ import {
within,
} from "talk-framework/testHelpers";
import {
BanUserMutation,
RemoveUserBanMutation,
UpdateUserRoleMutation,
} from "talk-admin/mutations";
import create from "../create";
import {
communityUsers,
@@ -35,29 +30,29 @@ beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/community");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
settings: createQueryResolverStub<QueryToSettingsResolver>(
() => settings
),
users: createQueryResolverStub<QueryToUsersResolver>(variables => {
expectAndFail(variables.role).toBeFalsy();
return communityUsers;
}),
viewer: createQueryResolverStub<QueryToViewerResolver>(
() => users.admins[0]
),
...resolver.Query,
},
};
const createTestRenderer = async (
params: CreateTestRendererParams<GQLResolver> = {}
) => {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
users: ({ variables }) => {
expectAndFail(variables.role).toBeFalsy();
return communityUsers;
},
viewer: () => users.admins[0],
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
@@ -73,10 +68,12 @@ it("renders community", async () => {
it("renders empty community", async () => {
const { container } = await createTestRenderer({
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(
() => emptyCommunityUsers
),
resolvers: {
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(
() => emptyCommunityUsers
),
},
},
});
expect(within(container).toJSON()).toMatchSnapshot();
@@ -84,9 +81,9 @@ it("renders empty community", async () => {
it("filter by role", async () => {
const { container } = await createTestRenderer({
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
resolvers: createResolversStub<GQLResolver>({
Query: {
users: ({ variables, callCount }) => {
switch (callCount) {
case 0:
return communityUsers;
@@ -94,9 +91,9 @@ it("filter by role", async () => {
expectAndFail(variables.role).toBe(GQLUSER_ROLE.COMMENTER);
return emptyCommunityUsers;
}
}
),
},
},
},
}),
});
const selectField = within(container).getByLabelText("Search by role");
@@ -127,21 +124,24 @@ it("can't change viewer role", async () => {
it("change user role", async () => {
const user = users.commenters[0];
const updateUserRole = createMutationResolverStub<
typeof UpdateUserRoleMutation
>(variables => {
expectAndFail(variables).toMatchObject({
userID: user.id,
role: GQLUSER_ROLE.STAFF,
});
const userRecord = merge({}, user, { role: variables.role });
return {
user: userRecord,
};
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateUserRole: ({ variables }) => {
expectAndFail(variables).toMatchObject({
userID: user.id,
role: GQLUSER_ROLE.STAFF,
});
const userRecord = pureMerge<typeof user>(user, {
role: variables.role,
});
return {
user: userRecord,
};
},
},
});
const { container } = await createTestRenderer({
Mutation: { updateUserRole },
resolvers,
});
const userRow = within(container).getByText(user.username!, {
@@ -165,29 +165,34 @@ it("change user role", async () => {
});
within(userRow).getByText("Staff");
expect(updateUserRole.called).toBe(true);
expect(resolvers.Mutation!.updateUserRole!.called).toBe(true);
});
it("can't change role as a moderator", async () => {
const viewer = users.moderators[0];
const { container } = await createTestRenderer({
Query: {
viewer: createQueryResolverStub<QueryToViewerResolver>(() => viewer),
},
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () => viewer,
},
}),
});
expect(() => within(container).getByLabelText("Change role")).toThrow();
});
it("load more", async () => {
const { container } = await createTestRenderer({
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
resolvers: createResolversStub<GQLResolver>({
Query: {
users: ({ callCount }) => {
switch (callCount) {
case 0:
return {
edges: [
{ node: users.admins[0], cursor: users.admins[0].createdAt },
{
node: users.admins[0],
cursor: users.admins[0].createdAt,
},
{
node: users.commenters[0],
cursor: users.commenters[0].createdAt,
@@ -212,9 +217,9 @@ it("load more", async () => {
},
};
}
}
),
},
},
},
}),
});
const loadMore = within(container).getByText("Load More");
TestRenderer.act(() => {
@@ -230,9 +235,9 @@ it("load more", async () => {
it("filter by search", async () => {
const { container } = await createTestRenderer({
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
resolvers: createResolversStub<GQLResolver>({
Query: {
users: ({ variables, callCount }) => {
switch (callCount) {
case 0:
return communityUsers;
@@ -240,9 +245,9 @@ it("filter by search", async () => {
expectAndFail(variables.query).toBe("search");
return emptyCommunityUsers;
}
}
),
},
},
},
}),
});
const searchField = within(container).getByLabelText("Search by username", {
@@ -264,9 +269,9 @@ it("filter by search", async () => {
it("filter by status", async () => {
const { container } = await createTestRenderer({
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(
(variables, callCount) => {
resolvers: createResolversStub<GQLResolver>({
Query: {
users: ({ variables, callCount }) => {
switch (callCount) {
case 0:
return communityUsers;
@@ -274,9 +279,9 @@ it("filter by status", async () => {
expectAndFail(variables.status).toBe("BANNED");
return emptyCommunityUsers;
}
}
),
},
},
},
}),
});
const statusField = within(container).getByLabelText(
@@ -314,25 +319,28 @@ it("can't change staff, moderator and admin status", async () => {
it("ban user", async () => {
const user = users.commenters[0];
const banUser = createMutationResolverStub<typeof BanUserMutation>(
variables => {
expectAndFail(variables).toMatchObject({
userID: user.id,
});
const userRecord = merge({}, user, {
status: {
current: user.status.current.concat(GQLUSER_STATUS.BANNED),
banned: { active: true },
},
});
return {
user: userRecord,
};
}
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
banUser: ({ variables }) => {
expectAndFail(variables).toMatchObject({
userID: user.id,
});
const userRecord = pureMerge<typeof user>(user, {
status: {
current: user.status.current.concat(GQLUSER_STATUS.BANNED),
ban: { active: true },
},
});
return {
user: userRecord,
};
},
},
});
const { container, testRenderer } = await createTestRenderer({
Mutation: { banUser },
resolvers,
});
const userRow = within(container).getByText(user.username!, {
@@ -368,32 +376,32 @@ it("ban user", async () => {
.getByText("Ban User")
.props.onClick();
within(userRow).getByText("Banned");
expect(banUser.called).toBe(true);
expect(resolvers.Mutation!.banUser!.called).toBe(true);
});
it("remove user ban", async () => {
const user = users.bannedCommenter;
const removeUserBan = createMutationResolverStub<
typeof RemoveUserBanMutation
>(variables => {
expectAndFail(variables).toMatchObject({
userID: user.id,
});
const userRecord = merge({}, user, {
status: {
current: user.status.current.filter(s => s !== GQLUSER_STATUS.BANNED),
banned: { active: false },
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
removeUserBan: ({ variables }) => {
expectAndFail(variables).toMatchObject({
userID: user.id,
});
const userRecord = pureMerge<typeof user>(user, {
status: {
current: user.status.current.filter(
s => s !== GQLUSER_STATUS.BANNED
),
ban: { active: false },
},
});
return {
user: userRecord,
};
},
});
return {
user: userRecord,
};
});
const { container } = await createTestRenderer({
Mutation: { removeUserBan },
},
Query: {
users: createQueryResolverStub<QueryToUsersResolver>(() => ({
users: () => ({
edges: [
{
node: user,
@@ -401,10 +409,14 @@ it("remove user ban", async () => {
},
],
pageInfo: { endCursor: null, hasNextPage: false },
})),
}),
},
});
const { container } = await createTestRenderer({
resolvers,
});
const userRow = within(container).getByText(user.username!, {
selector: "tr",
});
@@ -426,5 +438,5 @@ it("remove user ban", async () => {
});
within(userRow).getByText("Active");
expect(removeUserBan.called).toBe(true);
expect(resolvers.Mutation!.removeUserBan!.called).toBe(true);
});
@@ -1,8 +1,8 @@
import { cloneDeep, get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createSinonStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -16,25 +16,30 @@ beforeEach(() => {
replaceHistoryLocation("http://localhost/admin/configure/advanced");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const configureContainer = await waitForElement(() =>
within(testRenderer.root).getByTestID("configure-container")
);
@@ -45,12 +50,13 @@ const createTestRenderer = async (resolver: any = {}) => {
"configure-sideBar-saveChanges"
);
return {
context,
testRenderer,
configureContainer,
advancedContainer,
saveChangesButton,
};
};
}
it("renders configure advanced", async () => {
const { configureContainer } = await createTestRenderer();
@@ -58,25 +64,22 @@ it("renders configure advanced", async () => {
});
it("change custom css", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.customCSSURL).toEqual("./custom.css");
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.customCSSURL).toEqual("./custom.css");
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
advancedContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
resolvers,
});
const customCSSField = within(advancedContainer).getByLabelText("Custom CSS");
@@ -99,29 +102,26 @@ it("change custom css", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change permitted domains to be empty", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.domains).toEqual([]);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.domains).toEqual([]);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
advancedContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
resolvers,
});
const permittedDomainsField = within(advancedContainer).getByLabelText(
@@ -146,32 +146,29 @@ it("change permitted domains to be empty", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change permitted domains to include more domains", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.domains).toEqual([
"localhost:8080",
"localhost:3000",
]);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.domains).toEqual([
"localhost:8080",
"localhost:3000",
]);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
advancedContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
resolvers,
});
const permittedDomainsField = within(advancedContainer).getByLabelText(
@@ -196,5 +193,5 @@ it("change permitted domains to include more domains", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
+146 -128
View File
@@ -1,10 +1,13 @@
import { cloneDeep, get, merge } from "lodash";
import { cloneDeep } from "lodash";
import { ReactTestInstance } from "react-test-renderer";
import sinon from "sinon";
import { timeout } from "talk-common/utils";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createSinonStub,
inputPredicate,
createResolversStub,
CreateTestRendererParams,
limitSnapshotTo,
replaceHistoryLocation,
waitForElement,
@@ -14,29 +17,44 @@ import {
import create from "../create";
import { settingsWithEmptyAuth, users } from "../fixtures";
/**
* This is depreacted, do not use it anymore.
* @deprecated
*/
const deprecatedInputPredicate = (nameOrID: string) => (
n: ReactTestInstance
) => {
return (
[n.props.name, n.props.id].indexOf(nameOrID) > -1 &&
["input", "button"].indexOf(n.type as string) > -1
);
};
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/configure/auth");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(
merge({}, settingsWithEmptyAuth, get(resolver, "Query.settings"))
),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settingsWithEmptyAuth,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const configureContainer = await waitForElement(() =>
@@ -45,8 +63,8 @@ const createTestRenderer = async (resolver: any = {}) => {
const authContainer = await waitForElement(() =>
within(configureContainer).getByTestID("configure-authContainer")
);
return { testRenderer, configureContainer, authContainer };
};
return { context, testRenderer, configureContainer, authContainer };
}
it("renders configure auth", async () => {
const { configureContainer } = await createTestRenderer();
@@ -55,32 +73,34 @@ it("renders configure auth", async () => {
it("regenerate sso key", async () => {
const { testRenderer } = await createTestRenderer({
Mutation: {
regenerateSSOKey: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
resolvers: createResolversStub<GQLResolver>({
Mutation: {
regenerateSSOKey: () => {
return {
settings: merge({}, settingsWithEmptyAuth, {
auth: {
integrations: {
sso: {
key: "==GENERATED_KEY==",
keyGeneratedAt: "2018-11-12T23:26:06.239Z",
settings: pureMerge<typeof settingsWithEmptyAuth>(
settingsWithEmptyAuth,
{
auth: {
integrations: {
sso: {
key: "==GENERATED_KEY==",
keyGeneratedAt: "2018-11-12T23:26:06.239Z",
},
},
},
},
}),
clientMutationId: data.input.clientMutationId,
}
),
};
})
),
},
},
},
}),
});
testRenderer.root
.find(inputPredicate("auth.integrations.sso.enabled"))
.find(deprecatedInputPredicate("auth.integrations.sso.enabled"))
.props.onChange({});
testRenderer.root
.find(inputPredicate("configure-auth-sso-regenerate"))
.find(deprecatedInputPredicate("configure-auth-sso-regenerate"))
.props.onClick();
await timeout();
@@ -95,7 +115,7 @@ it("prevents admin lock out", async () => {
// Let's disable local auth.
testRenderer.root
.find(inputPredicate("auth.integrations.local.enabled"))
.find(deprecatedInputPredicate("auth.integrations.local.enabled"))
.props.onChange();
// Send form
@@ -109,10 +129,10 @@ it("prevents admin lock out", async () => {
it("prevents stream lock out", async () => {
let settingsRecord = cloneDeep(settingsWithEmptyAuth);
const { testRenderer } = await createTestRenderer({
Mutation: {
updateSettings: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.auth.integrations.local).toEqual({
resolvers: createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.auth!.integrations!.local).toEqual({
enabled: true,
allowRegistration: true,
targetFilter: {
@@ -120,14 +140,13 @@ it("prevents stream lock out", async () => {
stream: false,
},
});
settingsRecord = merge({}, settingsRecord, data.input.settings);
settingsRecord = pureMerge(settingsRecord, variables.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
),
},
},
},
}),
});
const origConfirm = window.confirm;
const stubContinue = sinon.stub().returns(true);
@@ -137,7 +156,9 @@ it("prevents stream lock out", async () => {
window.confirm = stubCancel;
// Let's disable stream target in local auth.
testRenderer.root
.find(inputPredicate("auth.integrations.local.targetFilter.stream"))
.find(
deprecatedInputPredicate("auth.integrations.local.targetFilter.stream")
)
.props.onChange();
// Send form
@@ -154,7 +175,9 @@ it("prevents stream lock out", async () => {
window.confirm = stubContinue;
// Let's enable stream target in local auth.
testRenderer.root
.find(inputPredicate("auth.integrations.local.targetFilter.stream"))
.find(
deprecatedInputPredicate("auth.integrations.local.targetFilter.stream")
)
.props.onChange();
// Send form
@@ -167,83 +190,74 @@ it("prevents stream lock out", async () => {
});
it("change settings", async () => {
let settingsRecord = cloneDeep(settingsWithEmptyAuth);
const { testRenderer } = await createTestRenderer({
Query: {
discoverOIDCConfiguration: createSinonStub(s =>
s.callsFake((_: any, data: any) => {
expectAndFail(data).toEqual({ issuer: "http://issuer.com" });
resolvers: createResolversStub<GQLResolver>({
Query: {
discoverOIDCConfiguration: ({ variables }) => {
expectAndFail(variables).toEqual({ issuer: "http://issuer.com" });
return {
issuer: "http://issuer.com",
tokenURL: "http://issuer.com/tokenURL",
jwksURI: "http://issuer.com/jwksURI",
authorizationURL: "http://issuer.com/authorizationURL",
};
})
),
},
Mutation: {
updateSettings: createSinonStub(
s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(
data.input.settings.auth.integrations.facebook
).toEqual({
enabled: true,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
clientID: "myClientID",
clientSecret: "myClientSecret",
});
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
}),
s =>
s.onSecondCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.auth.integrations.oidc).toEqual({
enabled: true,
allowRegistration: false,
targetFilter: {
admin: true,
stream: true,
},
name: "name",
clientID: "clientID",
clientSecret: "clientSecret",
issuer: "http://issuer.com",
jwksURI: "http://issuer.com/jwksURI",
authorizationURL: "http://issuer.com/authorizationURL",
tokenURL: "http://issuer.com/tokenURL",
});
(settingsRecord.auth.integrations.oidc as any) = merge(
settingsRecord.auth.integrations.oidc,
data.input.configuration
);
return {
integration: settingsRecord.auth.integrations.oidc,
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
),
},
},
},
Mutation: {
updateSettings: ({ variables, callCount }) => {
switch (callCount) {
case 0:
expectAndFail(
variables.settings.auth!.integrations!.facebook
).toEqual({
enabled: true,
allowRegistration: true,
targetFilter: {
admin: true,
stream: true,
},
clientID: "myClientID",
clientSecret: "myClientSecret",
});
return {
settings: pureMerge(settingsWithEmptyAuth, variables.settings),
};
default:
expectAndFail(
variables.settings.auth!.integrations!.oidc
).toEqual({
enabled: true,
allowRegistration: false,
targetFilter: {
admin: true,
stream: true,
},
name: "name",
clientID: "clientID",
clientSecret: "clientSecret",
issuer: "http://issuer.com",
jwksURI: "http://issuer.com/jwksURI",
authorizationURL: "http://issuer.com/authorizationURL",
tokenURL: "http://issuer.com/tokenURL",
});
return {
settings: pureMerge(settingsWithEmptyAuth, variables.settings),
};
}
},
},
}),
});
// Let's change some facebook settings.
testRenderer.root
.find(inputPredicate("auth.integrations.facebook.enabled"))
.find(deprecatedInputPredicate("auth.integrations.facebook.enabled"))
.props.onChange({});
testRenderer.root
.find(inputPredicate("auth.integrations.facebook.clientID"))
.find(deprecatedInputPredicate("auth.integrations.facebook.clientID"))
.props.onChange("myClientID");
testRenderer.root
.find(inputPredicate("auth.integrations.facebook.clientSecret"))
.find(deprecatedInputPredicate("auth.integrations.facebook.clientSecret"))
.props.onChange("myClientSecret");
expect(
limitSnapshotTo("configure-auth-facebook-container", testRenderer.toJSON())
@@ -262,18 +276,20 @@ it("change settings", async () => {
// Disable other fields while submitting
// We are only testing for one here right now..
expect(
testRenderer.root.find(inputPredicate("auth.integrations.facebook.enabled"))
.props.disabled
testRenderer.root.find(
deprecatedInputPredicate("auth.integrations.facebook.enabled")
).props.disabled
).toBe(true);
await timeout();
expect(
testRenderer.root.find(inputPredicate("auth.integrations.facebook.enabled"))
.props.disabled
testRenderer.root.find(
deprecatedInputPredicate("auth.integrations.facebook.enabled")
).props.disabled
).toBe(false);
// Now let's enable oidc
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.enabled"))
.find(deprecatedInputPredicate("auth.integrations.oidc.enabled"))
.props.onChange({});
expect(
@@ -288,21 +304,21 @@ it("change settings", async () => {
// Fill form
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.name"))
.find(deprecatedInputPredicate("auth.integrations.oidc.name"))
.props.onChange("name");
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.clientID"))
.find(deprecatedInputPredicate("auth.integrations.oidc.clientID"))
.props.onChange("clientID");
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.clientSecret"))
.find(deprecatedInputPredicate("auth.integrations.oidc.clientSecret"))
.props.onChange("clientSecret");
testRenderer.root
.find(inputPredicate("auth.integrations.oidc.issuer"))
.find(deprecatedInputPredicate("auth.integrations.oidc.issuer"))
.props.onChange("http://issuer.com");
// Discover the rest.
testRenderer.root
.find(inputPredicate("configure-auth-oidc-discover"))
.find(deprecatedInputPredicate("configure-auth-oidc-discover"))
.props.onClick();
await timeout();
@@ -315,12 +331,14 @@ it("change settings", async () => {
// Disable other fields while submitting
// We are only testing for one here right now..
expect(
testRenderer.root.find(inputPredicate("auth.integrations.oidc.enabled"))
.props.disabled
testRenderer.root.find(
deprecatedInputPredicate("auth.integrations.oidc.enabled")
).props.disabled
).toBe(true);
await timeout();
expect(
testRenderer.root.find(inputPredicate("auth.integrations.oidc.enabled"))
.props.disabled
testRenderer.root.find(
deprecatedInputPredicate("auth.integrations.oidc.enabled")
).props.disabled
).toBe(false);
});
@@ -1,10 +1,10 @@
import { cloneDeep, get, merge } from "lodash";
import sinon from "sinon";
import { ERROR_CODES } from "talk-common/errors";
import { pureMerge } from "talk-common/utils";
import { InvalidRequestError } from "talk-framework/lib/errors";
import { GQLResolver } from "talk-framework/schema";
import {
createSinonStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -18,27 +18,27 @@ beforeEach(() => {
replaceHistoryLocation("http://localhost/admin/configure/general");
});
const createTestRenderer = async (
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const configureContainer = await waitForElement(() =>
@@ -51,12 +51,13 @@ const createTestRenderer = async (
"configure-sideBar-saveChanges"
);
return {
context,
testRenderer,
configureContainer,
generalContainer,
saveChangesButton,
};
};
}
it("renders configure general", async () => {
const { configureContainer } = await createTestRenderer();
@@ -64,28 +65,25 @@ it("renders configure general", async () => {
});
it("change site wide commenting", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.disableCommenting).toEqual({
enabled: true,
message: "Closing message",
});
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.disableCommenting).toEqual({
enabled: true,
message: "Closing message",
});
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
generalContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
resolvers,
});
const sitewideCommentingContainer = within(generalContainer).getAllByText(
@@ -121,34 +119,32 @@ it("change site wide commenting", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change community guidlines", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.communityGuidelines.content).toEqual(
"This is the community guidlines summary"
);
expectAndFail(data.input.settings.communityGuidelines.enabled).toEqual(
true
);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.communityGuidelines!.content).toEqual(
"This is the community guidlines summary"
);
expectAndFail(variables.settings.communityGuidelines!.enabled).toEqual(
true
);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
generalContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
resolvers,
});
const guidelinesContainer = within(generalContainer).getAllByText(
@@ -182,32 +178,27 @@ it("change community guidlines", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change closed stream message", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.closeCommenting.message).toEqual(
"The stream has been closed"
);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.closeCommenting!.message).toEqual(
"The stream has been closed"
);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
generalContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
});
} = await createTestRenderer({ resolvers });
const contentField = within(generalContainer).getByLabelText(
"Closed Stream Message"
@@ -226,33 +217,28 @@ it("change closed stream message", async () => {
// Wait for submission to be finished
await wait(() => {
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
});
it("change comment editing time", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.editCommentWindowLength).toEqual(
108000
);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.editCommentWindowLength).toEqual(
108000
);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
generalContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
});
} = await createTestRenderer({ resolvers });
const durationFieldset = within(generalContainer).getByText(
"Comment Edit Timeframe",
@@ -296,34 +282,31 @@ it("change comment editing time", async () => {
// Wait for submission to be finished
await wait(() => {
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
});
it("change comment length limitations", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.charCount).toEqual({
enabled: true,
min: null,
max: 3000,
});
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.charCount).toEqual({
enabled: true,
min: null,
max: 3000,
});
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
generalContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
resolvers,
});
const commentLengthContainer = within(generalContainer).getByText(
@@ -389,33 +372,28 @@ it("change comment length limitations", async () => {
expect(minField.props.disabled).toBe(false);
expect(maxField.props.disabled).toBe(false);
});
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change closing comment streams", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.closeCommenting.auto).toEqual(true);
expectAndFail(data.input.settings.closeCommenting.timeout).toEqual(
2592000
);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.closeCommenting!.auto).toEqual(true);
expectAndFail(variables.settings.closeCommenting!.timeout).toEqual(
2592000
);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
generalContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
});
} = await createTestRenderer({ resolvers });
const closingCommentStreamsContainer = within(generalContainer).getByText(
"Closing Comment Streams",
@@ -464,23 +442,22 @@ it("change closing comment streams", async () => {
expect(valueField.props.disabled).toBe(false);
expect(unitField.props.disabled).toBe(false);
});
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("handle server error", async () => {
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
throw new InvalidRequestError({ code: ERROR_CODES.INTERNAL_ERROR });
})
);
const { configureContainer, generalContainer } = await createTestRenderer(
{
Mutation: {
updateSettings: updateSettingsStub,
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: () => {
throw new InvalidRequestError({ code: ERROR_CODES.INTERNAL_ERROR });
},
},
{ muteNetworkErrors: true }
);
});
const { configureContainer, generalContainer } = await createTestRenderer({
resolvers,
muteNetworkErrors: true,
});
const contentField = within(generalContainer).getByLabelText(
"Closed Stream Message"
@@ -1,8 +1,8 @@
import { cloneDeep, get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createSinonStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -16,23 +16,27 @@ beforeEach(() => {
replaceHistoryLocation("http://localhost/admin/configure/moderation");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const configureContainer = await waitForElement(() =>
@@ -50,7 +54,7 @@ const createTestRenderer = async (resolver: any = {}) => {
moderationContainer,
saveChangesButton,
};
};
}
it("renders configure moderation", async () => {
const { configureContainer } = await createTestRenderer();
@@ -58,30 +62,25 @@ it("renders configure moderation", async () => {
});
it("change akismet settings", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.integrations.akismet).toEqual({
enabled: true,
key: "my api key",
site: "https://coralproject.net",
});
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.integrations!.akismet).toEqual({
enabled: true,
key: "my api key",
site: "https://coralproject.net",
});
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
moderationContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
});
} = await createTestRenderer({ resolvers });
const akismetContainer = within(moderationContainer).getByText(
"Akismet Spam Detection Filter",
@@ -137,48 +136,41 @@ it("change akismet settings", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change perspective settings", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(
s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.integrations.perspective).toEqual({
doNotStore: false,
enabled: true,
endpoint: "https://custom-endpoint.net",
key: "my api key",
threshold: 0.1,
});
settingsRecord = merge(settingsRecord, data.input.settings);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables, callCount }) => {
switch (callCount) {
case 0:
expectAndFail(variables.settings.integrations!.perspective).toEqual(
{
doNotStore: false,
enabled: true,
endpoint: "https://custom-endpoint.net",
key: "my api key",
threshold: 0.1,
}
);
break;
default:
expectAndFail(
variables.settings.integrations!.perspective!.threshold
).toBeNull();
}
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
settings: pureMerge(settings, variables.settings),
};
}),
s =>
s.onSecondCall().callsFake((_: any, data: any) => {
expectAndFail(
data.input.settings.integrations.perspective.threshold
).toBeNull();
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
},
},
});
const {
configureContainer,
moderationContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
});
} = await createTestRenderer({ resolvers });
const perspectiveContainer = within(moderationContainer).getByText(
"Perspective Toxic Comment Filter",
@@ -258,7 +250,7 @@ it("change perspective settings", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.calledOnce).toBe(true);
expect(resolvers.Mutation!.updateSettings!.calledOnce).toBe(true);
// Use default threshold.
thresholdField.props.onChange("");
@@ -278,5 +270,5 @@ it("change perspective settings", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.calledTwice).toBe(true);
expect(resolvers.Mutation!.updateSettings!.calledTwice).toBe(true);
});
@@ -1,8 +1,8 @@
import { cloneDeep, get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import { GQLResolver } from "talk-framework/schema";
import {
createSinonStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
@@ -16,23 +16,27 @@ beforeEach(() => {
replaceHistoryLocation("http://localhost/admin/configure/organization");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const configureContainer = await waitForElement(() =>
@@ -50,7 +54,7 @@ const createTestRenderer = async (resolver: any = {}) => {
organizationContainer,
saveChangesButton,
};
};
}
it("renders configure organization", async () => {
const { configureContainer } = await createTestRenderer();
@@ -58,28 +62,23 @@ it("renders configure organization", async () => {
});
it("change organization name", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.organization.name).toEqual(
"Coral Test"
);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.organization!.name).toEqual(
"Coral Test"
);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
organizationContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
});
} = await createTestRenderer({ resolvers });
const organizationNameField = within(organizationContainer).getByLabelText(
"Organization Name"
@@ -119,32 +118,27 @@ it("change organization name", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
it("change organization contact email", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.organization.contactEmail).toEqual(
"test@coralproject.net"
);
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.organization!.contactEmail).toEqual(
"test@coralproject.net"
);
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
organizationContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
});
} = await createTestRenderer({ resolvers });
const organizationEmailField = within(organizationContainer).getByLabelText(
"Organization Email"
@@ -184,5 +178,5 @@ it("change organization contact email", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
@@ -1,8 +1,8 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import { GQLUSER_ROLE } from "talk-framework/schema";
import { pureMerge } from "talk-common/utils";
import { GQLResolver, GQLUSER_ROLE } from "talk-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
waitForElement,
within,
@@ -15,40 +15,43 @@ beforeEach(() => {
replaceHistoryLocation("http://localhost/admin/configure/general");
});
const createTestRenderer = async (
resolver: any = {},
options: { muteNetworkErrors?: boolean } = {}
) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
},
};
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
muteNetworkErrors: options.muteNetworkErrors,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return {
testRenderer,
};
};
}
it("denies access to moderators", async () => {
const deniedRoles = [GQLUSER_ROLE.MODERATOR];
for (const r of deniedRoles) {
const { testRenderer } = await createTestRenderer({
Query: {
viewer: sinon.stub().returns({ ...users.admins[0], role: r }),
},
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () => ({ ...viewer, role: r }),
},
}),
});
await waitForElement(() =>
within(testRenderer.root).getByText("Sign in with a different account")
@@ -60,9 +63,11 @@ it("allows access to admins", async () => {
const deniedRoles = [GQLUSER_ROLE.ADMIN];
for (const r of deniedRoles) {
const { testRenderer } = await createTestRenderer({
Query: {
viewer: sinon.stub().returns({ ...users.admins[0], role: r }),
},
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () => ({ ...viewer, role: r }),
},
}),
});
await waitForElement(() =>
within(testRenderer.root).getByTestID("configure-container")
@@ -1,14 +1,14 @@
import { cloneDeep, get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
createSinonStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
wait,
waitForElement,
within,
} from "talk-framework/testHelpers";
import { GQLResolver } from "talk-framework/schema";
import create from "../create";
import { settings, users } from "../fixtures";
@@ -16,23 +16,27 @@ beforeEach(() => {
replaceHistoryLocation("http://localhost/admin/configure/wordList");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
viewer: sinon.stub().returns(users.admins[0]),
},
};
const viewer = users.admins[0];
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const configureContainer = await waitForElement(() =>
@@ -50,7 +54,7 @@ const createTestRenderer = async (resolver: any = {}) => {
wordListContainer,
saveChangesButton,
};
};
}
it("renders configure wordList", async () => {
const { configureContainer } = await createTestRenderer();
@@ -58,28 +62,25 @@ it("renders configure wordList", async () => {
});
it("change banned and suspect words", async () => {
let settingsRecord = cloneDeep(settings);
const updateSettingsStub = createSinonStub(s =>
s.onFirstCall().callsFake((_: any, data: any) => {
expectAndFail(data.input.settings.wordList).toEqual({
banned: ["Fuck", "Asshole"],
suspect: ["idiot", "shame"],
});
settingsRecord = merge(settingsRecord, data.input.settings);
return {
settings: settingsRecord,
clientMutationId: data.input.clientMutationId,
};
})
);
const resolvers = createResolversStub<GQLResolver>({
Mutation: {
updateSettings: ({ variables }) => {
expectAndFail(variables.settings.wordList).toEqual({
banned: ["Fuck", "Asshole"],
suspect: ["idiot", "shame"],
});
return {
settings: pureMerge(settings, variables.settings),
};
},
},
});
const {
configureContainer,
wordListContainer,
saveChangesButton,
} = await createTestRenderer({
Mutation: {
updateSettings: updateSettingsStub,
},
resolvers,
});
const bannedField = within(wordListContainer).getByLabelText(
@@ -110,5 +111,5 @@ it("change banned and suspect words", async () => {
});
// Should have successfully sent with server.
expect(updateSettingsStub.called).toBe(true);
expect(resolvers.Mutation!.updateSettings!.called).toBe(true);
});
+7 -62
View File
@@ -1,67 +1,12 @@
import { EventEmitter2 } from "eventemitter2";
import { IResolvers } from "graphql-tools";
import React from "react";
import TestRenderer, { ReactTestRenderer } from "react-test-renderer";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import EntryContainer from "talk-admin/containers/EntryContainer";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { createUUIDGenerator } from "talk-framework/testHelpers";
import { GQLResolver } from "talk-framework/schema";
import {
createTestRenderer,
CreateTestRendererParams,
} from "talk-framework/testHelpers";
import createEnvironment from "./createEnvironment";
import createFluentBundle from "./createFluentBundle";
import createNodeMock from "./createNodeMock";
interface CreateParams {
logNetwork?: boolean;
resolvers?: IResolvers<any, any>;
muteNetworkErrors?: boolean;
initLocalState?: (
local: RecordProxy,
source: RecordSourceProxy,
environment: Environment
) => void;
}
export default function create(params: CreateParams) {
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: params.logNetwork,
resolvers: params.resolvers,
muteNetworkErrors: params.muteNetworkErrors,
initLocalState: (localRecord, source, env) => {
if (params.initLocalState) {
params.initLocalState(localRecord, source, env);
}
},
});
const context: TalkContext = {
relayEnvironment: environment,
locales: ["en-US"],
localeBundles: [createFluentBundle()],
localStorage: createPromisifiedStorage(),
sessionStorage: createPromisifiedStorage(),
rest: new RestClient("http://localhost/api"),
postMessage: new PostMessageService(),
browserInfo: { ios: false },
uuidGenerator: createUUIDGenerator(),
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
clearSession: () => Promise.resolve(),
};
let testRenderer: ReactTestRenderer;
TestRenderer.act(() => {
testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<EntryContainer />
</TalkContextProvider>,
{ createNodeMock }
);
});
return { context, testRenderer: testRenderer! };
export default function create(params: CreateTestRendererParams<GQLResolver>) {
return createTestRenderer<GQLResolver>("admin", <EntryContainer />, params);
}
@@ -1,30 +0,0 @@
import { IResolvers } from "graphql-tools";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import { createRelayEnvironment } from "talk-framework/testHelpers";
interface CreateEnvironmentParams {
logNetwork?: boolean;
resolvers?: IResolvers<any, any>;
muteNetworkErrors?: boolean;
initLocalState?: (
local: RecordProxy,
source: RecordSourceProxy,
environment: Environment
) => void;
}
export default function createEnvironment(params: CreateEnvironmentParams) {
return createRelayEnvironment({
network: {
logNetwork: params.logNetwork,
muteNetworkErrors: params.muteNetworkErrors,
resolvers: params.resolvers || {},
projectName: "tenant",
},
initLocalState: (localRecord, source, environment) => {
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
}
@@ -1,10 +0,0 @@
import path from "path";
import { createFluentBundle } from "talk-framework/testHelpers";
export default function create() {
return createFluentBundle(
"admin",
path.resolve(__dirname, "../../../../locales/en-US")
);
}
@@ -1,13 +0,0 @@
import { noop } from "lodash";
import { ReactElement } from "react";
export default function createNodeMock(element: ReactElement<any>) {
if (element.type === "div") {
return {
innerHtml: "",
className: "",
focus: noop,
};
}
return null;
}
@@ -1,8 +1,12 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
createSinonStub,
GQLResolver,
UserToCommentModerationActionHistoryResolver,
} from "talk-framework/schema";
import {
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
waitForElement,
@@ -17,71 +21,73 @@ beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/configure/auth");
});
const commentModerationActionHistory = createSinonStub(
s => s.throws(),
s =>
s.withArgs({ first: 5 }).returns({
edges: [
{
node: moderationActions[0],
cursor: moderationActions[0].createdAt,
},
{
node: moderationActions[1],
cursor: moderationActions[1].createdAt,
},
],
pageInfo: {
endCursor: moderationActions[1].createdAt,
hasNextPage: true,
},
}),
s =>
s
.withArgs({
first: 10,
after: moderationActions[1].createdAt,
})
.returns({
edges: [
{
node: moderationActions[2],
cursor: moderationActions[2].createdAt,
},
],
pageInfo: {
endCursor: moderationActions[2].createdAt,
hasNextPage: false,
},
})
);
const viewer = users.admins[0];
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
viewer: sinon.stub().returns({
...users.admins[0],
commentModerationActionHistory,
}),
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
},
};
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () =>
pureMerge(viewer, {
commentModerationActionHistory: createQueryResolverStub<
UserToCommentModerationActionHistoryResolver
>(({ variables }) => {
if (variables.first === 5) {
return {
edges: [
{
node: moderationActions[0],
cursor: moderationActions[0].createdAt,
},
{
node: moderationActions[1],
cursor: moderationActions[1].createdAt,
},
],
pageInfo: {
endCursor: moderationActions[1].createdAt,
hasNextPage: true,
},
};
}
expectAndFail(variables).toEqual({
first: 10,
after: moderationActions[1].createdAt,
});
return {
edges: [
{
node: moderationActions[2],
cursor: moderationActions[2].createdAt,
},
],
pageInfo: {
endCursor: moderationActions[2].createdAt,
hasNextPage: false,
},
};
}),
}),
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("decisionHistory-toggle"));
return testRenderer;
};
}
async function createTestRendererAndOpenPopover() {
const testRenderer = await createTestRenderer();
@@ -1,8 +1,17 @@
import { get, merge } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
createSinonStub,
GQLCOMMENT_STATUS,
GQLResolver,
ModerationQueueToCommentsResolver,
MutationToAcceptCommentResolver,
MutationToRejectCommentResolver,
QueryToCommentResolver,
} from "talk-framework/schema";
import {
createMutationResolverStub,
createQueryResolverStub,
createResolversStub,
CreateTestRendererParams,
replaceHistoryLocation,
toJSON,
waitForElement,
@@ -20,43 +29,37 @@ import {
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/moderate");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
...resolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
moderationQueues: sinon
.stub()
.returns(
merge(
{},
emptyModerationQueues,
get(resolver, "Query.moderationQueues")
)
),
comments:
get(resolver, "Query.comments") ||
sinon.stub().returns(emptyRejectedComments),
viewer: sinon.stub().returns(users.admins[0]),
},
};
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
moderationQueues: () => emptyModerationQueues,
comments: () => emptyRejectedComments,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
return testRenderer;
};
}
describe("navigation bar", () => {
it("renders navigation bar (empty queues)", async () => {
@@ -78,48 +81,16 @@ describe("reported queue", () => {
it("renders reported queue with comments", async () => {
const testRenderer = await createTestRenderer({
Query: {
moderationQueues: {
reported: {
count: 2,
comments: sinon.stub().callsFake(data => {
expectAndFail(data).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}),
},
},
},
});
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
expect(toJSON(getByTestID("moderate-main-container"))).toMatchSnapshot();
});
it("renders reported queue with comments and load more", async () => {
const testRenderer = await createTestRenderer({
Query: {
moderationQueues: {
reported: {
count: 2,
comments: createSinonStub(
s =>
s.onFirstCall().callsFake(data => {
expectAndFail(data).toEqual({ first: 5 });
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
@@ -133,34 +104,75 @@ describe("reported queue", () => {
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: true,
},
};
}),
s =>
s.onSecondCall().callsFake(data => {
expectAndFail(data).toEqual({
first: 10,
after: reportedComments[1].createdAt,
});
return {
edges: [
{
node: reportedComments[2],
cursor: reportedComments[2].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[2].createdAt,
hasNextPage: false,
},
};
})
),
},
}) as any,
},
}),
},
}),
});
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
expect(toJSON(getByTestID("moderate-main-container"))).toMatchSnapshot();
});
it("renders reported queue with comments and load more", async () => {
const moderationQueuesStub = pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<ModerationQueueToCommentsResolver>(
({ variables, callCount }) => {
switch (callCount) {
case 0:
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: true,
},
};
default:
expectAndFail(variables).toEqual({
first: 10,
after: reportedComments[1].createdAt,
});
return {
edges: [
{
node: reportedComments[2],
cursor: reportedComments[2].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[2].createdAt,
hasNextPage: false,
},
};
}
}
) as any,
},
});
const testRenderer = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () => moderationQueuesStub,
},
}),
});
const moderateContainer = await waitForElement(() =>
within(testRenderer.root).getByTestID("moderate-container")
);
@@ -194,57 +206,62 @@ describe("reported queue", () => {
});
it("accepts comment in reported queue", async () => {
const acceptCommentStub = sinon.stub().callsFake((_, data) => {
expectAndFail(data).toMatchObject({
input: {
commentID: reportedComments[0].id,
commentRevisionID: reportedComments[0].revision.id,
},
const acceptCommentStub = createMutationResolverStub<
MutationToAcceptCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: reportedComments[0].id,
commentRevisionID: reportedComments[0].revision.id,
});
return {
comment: {
id: reportedComments[0].id,
status: "ACCEPTED",
status: GQLCOMMENT_STATUS.ACCEPTED,
},
moderationQueues: merge({}, emptyModerationQueues, {
moderationQueues: pureMerge(emptyModerationQueues, {
reported: {
count: 1,
},
}),
clientMutationId: data.input.clientMutationId,
};
});
const testRenderer = await createTestRenderer({
Query: {
moderationQueues: {
reported: {
count: 2,
comments: sinon.stub().callsFake(data => {
expectAndFail(data).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
const moderationQueuesStub = pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<ModerationQueueToCommentsResolver>(
({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
};
}),
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}
) as any,
},
});
const testRenderer = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () => moderationQueuesStub,
},
},
Mutation: {
acceptComment: acceptCommentStub,
},
Mutation: {
acceptComment: acceptCommentStub,
},
}),
});
const testID = `moderate-comment-${reportedComments[0].id}`;
@@ -271,57 +288,61 @@ describe("reported queue", () => {
});
it("rejects comment in reported queue", async () => {
const rejectCommentStub = sinon.stub().callsFake((_, data) => {
expectAndFail(data).toMatchObject({
input: {
commentID: reportedComments[0].id,
commentRevisionID: reportedComments[0].revision.id,
},
const rejectCommentStub = createMutationResolverStub<
MutationToRejectCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: reportedComments[0].id,
commentRevisionID: reportedComments[0].revision.id,
});
return {
comment: {
id: reportedComments[0].id,
status: "REJECTED",
status: GQLCOMMENT_STATUS.REJECTED,
},
moderationQueues: merge({}, emptyModerationQueues, {
moderationQueues: pureMerge(emptyModerationQueues, {
reported: {
count: 1,
},
}),
clientMutationId: data.input.clientMutationId,
};
});
const testRenderer = await createTestRenderer({
Query: {
moderationQueues: {
reported: {
count: 2,
comments: sinon.stub().callsFake(data => {
expectAndFail(data).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
resolvers: createResolversStub<GQLResolver>({
Query: {
moderationQueues: () =>
pureMerge(emptyModerationQueues, {
reported: {
count: 2,
comments: createQueryResolverStub<
ModerationQueueToCommentsResolver
>(({ variables }) => {
expectAndFail(variables).toEqual({ first: 5 });
return {
edges: [
{
node: reportedComments[0],
cursor: reportedComments[0].createdAt,
},
{
node: reportedComments[1],
cursor: reportedComments[1].createdAt,
},
],
pageInfo: {
endCursor: reportedComments[1].createdAt,
hasNextPage: false,
},
};
}) as any,
},
}),
},
},
},
Mutation: {
rejectComment: rejectCommentStub,
},
Mutation: {
rejectComment: rejectCommentStub,
},
}),
});
const testID = `moderate-comment-${reportedComments[0].id}`;
@@ -355,30 +376,32 @@ describe("rejected queue", () => {
it("renders rejected queue with comments", async () => {
const testRenderer = await createTestRenderer({
Query: {
comments: sinon.stub().callsFake((_, data) => {
expectAndFail(data).toEqual({
first: 5,
status: "REJECTED",
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
resolvers: createResolversStub<GQLResolver>({
Query: {
comments: ({ variables }) => {
expectAndFail(variables).toEqual({
first: 5,
status: "REJECTED",
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: false,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: false,
},
};
}),
},
};
},
},
}),
});
const { getByTestID } = within(testRenderer.root);
await waitForElement(() => getByTestID("moderate-container"));
@@ -387,53 +410,53 @@ describe("rejected queue", () => {
it("renders rejected queue with comments and load more", async () => {
const testRenderer = await createTestRenderer({
Query: {
comments: createSinonStub(
s =>
s.onFirstCall().callsFake((_, data) => {
expectAndFail(data).toEqual({
first: 5,
status: "REJECTED",
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
resolvers: createResolversStub<GQLResolver>({
Query: {
comments: ({ variables, callCount }) => {
switch (callCount) {
case 0:
expectAndFail(variables).toEqual({
first: 5,
status: GQLCOMMENT_STATUS.REJECTED,
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: true,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
};
default:
expectAndFail(variables).toEqual({
first: 10,
after: rejectedComments[1].createdAt,
status: GQLCOMMENT_STATUS.REJECTED,
});
return {
edges: [
{
node: rejectedComments[2],
cursor: rejectedComments[2].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[2].createdAt,
hasNextPage: false,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: true,
},
};
}),
s =>
s.onSecondCall().callsFake((_, data) => {
expectAndFail(data).toEqual({
first: 10,
after: rejectedComments[1].createdAt,
status: "REJECTED",
});
return {
edges: [
{
node: rejectedComments[2],
cursor: rejectedComments[2].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[2].createdAt,
hasNextPage: false,
},
};
})
),
},
};
}
},
},
}),
});
const moderateContainer = await waitForElement(() =>
@@ -469,55 +492,56 @@ describe("rejected queue", () => {
});
it("accepts comment in rejected queue", async () => {
const acceptCommentStub = sinon.stub().callsFake((_, data) => {
expectAndFail(data).toMatchObject({
input: {
commentID: rejectedComments[0].id,
commentRevisionID: rejectedComments[0].revision.id,
},
const acceptCommentStub = createMutationResolverStub<
MutationToAcceptCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: rejectedComments[0].id,
commentRevisionID: rejectedComments[0].revision.id,
});
return {
comment: {
id: rejectedComments[0].id,
status: "ACCEPTED",
status: GQLCOMMENT_STATUS.ACCEPTED,
},
moderationQueues: merge({}, emptyModerationQueues, {
moderationQueues: pureMerge(emptyModerationQueues, {
reported: {
count: 1,
},
}),
clientMutationId: data.input.clientMutationId,
};
});
const testRenderer = await createTestRenderer({
Query: {
comments: sinon.stub().callsFake((_, data) => {
expectAndFail(data).toEqual({
first: 5,
status: "REJECTED",
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
resolvers: createResolversStub<GQLResolver>({
Query: {
comments: ({ variables }) => {
expectAndFail(variables).toEqual({
first: 5,
status: "REJECTED",
});
return {
edges: [
{
node: rejectedComments[0],
cursor: rejectedComments[0].createdAt,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: false,
},
{
node: rejectedComments[1],
cursor: rejectedComments[1].createdAt,
},
],
pageInfo: {
endCursor: rejectedComments[1].createdAt,
hasNextPage: false,
},
};
}),
},
Mutation: {
acceptComment: acceptCommentStub,
},
};
},
},
Mutation: {
acceptComment: acceptCommentStub,
},
}),
});
const testID = `moderate-comment-${rejectedComments[0].id}`;
@@ -546,10 +570,12 @@ describe("rejected queue", () => {
describe("single comment view", () => {
const comment = rejectedComments[0];
const commentStub = sinon.stub().callsFake((_, data) => {
expectAndFail(data).toEqual({ id: comment.id });
return reportedComments[0];
});
const commentStub = createQueryResolverStub<QueryToCommentResolver>(
({ variables }) => {
expectAndFail(variables).toEqual({ id: comment.id });
return reportedComments[0];
}
);
beforeEach(() => {
replaceHistoryLocation(
@@ -559,8 +585,10 @@ describe("single comment view", () => {
it("renders single comment view", async () => {
const testRenderer = await createTestRenderer({
Query: {
comment: commentStub,
resolvers: {
Query: {
comment: commentStub,
},
},
});
const { getByTestID } = within(testRenderer.root);
@@ -571,29 +599,30 @@ describe("single comment view", () => {
});
it("accepts single comment", async () => {
const acceptCommentStub = sinon.stub().callsFake((_, data) => {
expectAndFail(data).toMatchObject({
input: {
commentID: comment.id,
commentRevisionID: comment.revision.id,
},
const acceptCommentStub = createMutationResolverStub<
MutationToAcceptCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: comment.id,
commentRevisionID: comment.revision.id,
});
return {
comment: {
id: comment.id,
status: "ACCEPTED",
status: GQLCOMMENT_STATUS.ACCEPTED,
},
moderationQueues: emptyModerationQueues,
clientMutationId: data.input.clientMutationId,
};
});
const testRenderer = await createTestRenderer({
Query: {
comment: commentStub,
},
Mutation: {
acceptComment: acceptCommentStub,
resolvers: {
Query: {
comment: commentStub,
},
Mutation: {
acceptComment: acceptCommentStub,
},
},
});
@@ -609,29 +638,30 @@ describe("single comment view", () => {
});
it("rejects single comment", async () => {
const rejectCommentStub = sinon.stub().callsFake((_, data) => {
expectAndFail(data).toMatchObject({
input: {
commentID: comment.id,
commentRevisionID: comment.revision.id,
},
const rejectCommentStub = createMutationResolverStub<
MutationToRejectCommentResolver
>(({ variables }) => {
expectAndFail(variables).toMatchObject({
commentID: comment.id,
commentRevisionID: comment.revision.id,
});
return {
comment: {
id: comment.id,
status: "REJECTED",
status: GQLCOMMENT_STATUS.REJECTED,
},
moderationQueues: emptyModerationQueues,
clientMutationId: data.input.clientMutationId,
};
});
const testRenderer = await createTestRenderer({
Query: {
comment: commentStub,
},
Mutation: {
rejectComment: rejectCommentStub,
resolvers: {
Query: {
comment: commentStub,
},
Mutation: {
rejectComment: rejectCommentStub,
},
},
});
@@ -1,9 +1,10 @@
import { get, merge } from "lodash";
import TestRenderer from "react-test-renderer";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
createSinonStub,
createMutationResolverStub,
createResolversStub,
CreateTestRendererParams,
findParentWithType,
replaceHistoryLocation,
waitForElement,
@@ -11,7 +12,12 @@ import {
within,
} from "talk-framework/testHelpers";
import { GQLSTORY_STATUS } from "talk-framework/schema";
import {
GQLResolver,
GQLSTORY_STATUS,
MutationToCloseStoryResolver,
MutationToOpenStoryResolver,
} from "talk-framework/schema";
import create from "../create";
import {
emptyStories,
@@ -21,38 +27,43 @@ import {
users,
} from "../fixtures";
const viewer = users.admins[0];
beforeEach(async () => {
replaceHistoryLocation("http://localhost/admin/stories");
});
const createTestRenderer = async (resolver: any = {}) => {
const resolvers = {
...resolver,
Query: {
settings: sinon
.stub()
.returns(merge({}, settings, get(resolver, "Query.settings"))),
stories: sinon.stub().callsFake((_, data) => {
expectAndFail(data.status).toBeFalsy();
return storyConnection;
}),
viewer: sinon.stub().returns(users.admins[0]),
...resolver.Query,
},
};
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer } = create({
// Set this to true, to see graphql responses.
logNetwork: false,
resolvers,
initLocalState: localRecord => {
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
stories: ({ variables }) => {
expectAndFail(variables.status).toBeFalsy();
return storyConnection;
},
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue(true, "loggedIn");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const container = await waitForElement(() =>
within(testRenderer.root).getByTestID("stories-container")
);
return { testRenderer, container };
};
}
it("renders stories", async () => {
const { container } = await createTestRenderer();
@@ -61,25 +72,30 @@ it("renders stories", async () => {
it("renders empty stories", async () => {
const { container } = await createTestRenderer({
Query: {
users: sinon.stub().returns(emptyStories),
},
resolvers: createResolversStub<GQLResolver>({
Query: {
users: () => emptyStories,
},
}),
});
expect(within(container).toJSON()).toMatchSnapshot();
});
it("filter by status", async () => {
const { container } = await createTestRenderer({
Query: {
stories: createSinonStub(
s => s.onFirstCall().returns(storyConnection),
s =>
s.onSecondCall().callsFake((_, data) => {
expectAndFail(data.status).toBe(GQLSTORY_STATUS.CLOSED);
return emptyStories;
})
),
},
resolvers: createResolversStub<GQLResolver>({
Query: {
stories: ({ variables, callCount }) => {
switch (callCount) {
case 0:
return storyConnection;
default:
expectAndFail(variables.status).toBe(GQLSTORY_STATUS.CLOSED);
return emptyStories;
}
},
},
}),
});
const selectField = within(container).getByLabelText("Search by status");
@@ -100,38 +116,42 @@ it("filter by status", async () => {
it("change story status", async () => {
const story = stories[1];
const openStory = sinon.stub().callsFake((_: any, data: any) => {
expectAndFail(data.input).toMatchObject({
id: story.id,
});
const storyRecord = merge({}, story, {
status: GQLSTORY_STATUS.OPEN,
createdAt: false,
isClosed: false,
});
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
});
const openStory = createMutationResolverStub<MutationToOpenStoryResolver>(
({ variables }) => {
expectAndFail(variables).toMatchObject({
id: story.id,
});
const storyRecord = pureMerge(story, {
status: GQLSTORY_STATUS.OPEN,
createdAt: false,
isClosed: false,
});
return {
story: storyRecord,
};
}
);
const closeStory = sinon.stub().callsFake((_: any, data: any) => {
expectAndFail(data.input).toMatchObject({
id: story.id,
});
const storyRecord = merge({}, story, {
status: GQLSTORY_STATUS.CLOSED,
createdAt: "2018-11-29T16:01:51.897Z",
isClosed: true,
});
return {
story: storyRecord,
clientMutationId: data.input.clientMutationId,
};
});
const closeStory = createMutationResolverStub<MutationToCloseStoryResolver>(
({ variables }) => {
expectAndFail(variables).toMatchObject({
id: story.id,
});
const storyRecord = pureMerge(story, {
status: GQLSTORY_STATUS.CLOSED,
createdAt: "2018-11-29T16:01:51.897Z",
isClosed: true,
});
return {
story: storyRecord,
};
}
);
const { container } = await createTestRenderer({
Mutation: { openStory, closeStory },
resolvers: {
Mutation: { openStory, closeStory },
},
});
const storyRow = within(container).getByText(story.metadata!.title!, {
@@ -174,23 +194,33 @@ it("change story status", async () => {
it("load more", async () => {
const { container } = await createTestRenderer({
Query: {
stories: createSinonStub(
s =>
s.onFirstCall().returns({
edges: [
{ node: stories[0], cursor: stories[0].createdAt },
{ node: stories[1], cursor: stories[1].createdAt },
],
pageInfo: { endCursor: stories[1].createdAt, hasNextPage: true },
}),
s =>
s.onSecondCall().returns({
edges: [{ node: stories[2], cursor: stories[2].createdAt }],
pageInfo: { endCursor: stories[2].createdAt, hasNextPage: false },
})
),
},
resolvers: createResolversStub<GQLResolver>({
Query: {
stories: ({ callCount }) => {
switch (callCount) {
case 0:
return {
edges: [
{ node: stories[0], cursor: stories[0].createdAt },
{ node: stories[1], cursor: stories[1].createdAt },
],
pageInfo: {
endCursor: stories[1].createdAt,
hasNextPage: true,
},
};
default:
return {
edges: [{ node: stories[2], cursor: stories[2].createdAt }],
pageInfo: {
endCursor: stories[2].createdAt,
hasNextPage: false,
},
};
}
},
},
}),
});
const loadMore = within(container).getByText("Load More");
TestRenderer.act(() => {
@@ -206,16 +236,19 @@ it("load more", async () => {
it("filter by search", async () => {
const { container } = await createTestRenderer({
Query: {
stories: createSinonStub(
s => s.onFirstCall().returns(storyConnection),
s =>
s.onSecondCall().callsFake((_, data) => {
expectAndFail(data.query).toBe("search");
return emptyStories;
})
),
},
resolvers: createResolversStub<GQLResolver>({
Query: {
stories: ({ variables, callCount }) => {
switch (callCount) {
case 0:
return storyConnection;
default:
expectAndFail(variables.query).toBe("search");
return emptyStories;
}
},
},
}),
});
const searchField = within(container).getByLabelText(
@@ -47,12 +47,11 @@ const ConfirmEmailField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -49,12 +49,11 @@ const SetPasswordField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -44,12 +44,11 @@ const EmailField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -50,12 +50,11 @@ const SetPasswordField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -48,12 +48,11 @@ const CreateUsernameField: StatelessComponent<Props> = props => (
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -1,6 +1,7 @@
import { get, merge } from "lodash";
import { get } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
createAccessToken,
wait,
@@ -26,11 +27,14 @@ async function createTestRenderer(
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
.returns(pureMerge(settings, get(customResolver, "Query.settings"))),
viewer: sinon
.stub()
.returns(
merge({ id: "me", profiles: [] }, get(customResolver, "Query.viewer"))
pureMerge(
{ id: "me", profiles: [] },
get(customResolver, "Query.viewer")
)
),
},
};
@@ -1,6 +1,7 @@
import { get, merge } from "lodash";
import { get } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
toJSON,
wait,
@@ -24,7 +25,7 @@ async function createTestRenderer(
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
.returns(pureMerge(settings, get(customResolver, "Query.settings"))),
},
};
+6 -57
View File
@@ -1,62 +1,11 @@
import { EventEmitter2 } from "eventemitter2";
import { IResolvers } from "graphql-tools";
import React from "react";
import TestRenderer from "react-test-renderer";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import AppQuery from "talk-auth/queries/AppQuery";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { createUUIDGenerator } from "talk-framework/testHelpers";
import {
createTestRenderer,
CreateTestRendererParams,
} from "talk-framework/testHelpers";
import createEnvironment from "./createEnvironment";
import createFluentBundle from "./createFluentBundle";
interface CreateParams {
logNetwork?: boolean;
muteNetworkErrors?: boolean;
resolvers?: IResolvers<any, any>;
initLocalState?: (
local: RecordProxy,
source: RecordSourceProxy,
environment: Environment
) => void;
}
export default function create(params: CreateParams) {
const environment = createEnvironment({
// Set this to true, to see graphql responses.
logNetwork: params.logNetwork,
muteNetworkErrors: params.muteNetworkErrors,
resolvers: params.resolvers,
initLocalState: (localRecord, source, env) => {
if (params.initLocalState) {
params.initLocalState(localRecord, source, env);
}
},
});
const context: TalkContext = {
relayEnvironment: environment,
locales: ["en-US"],
localeBundles: [createFluentBundle()],
localStorage: createPromisifiedStorage(),
sessionStorage: createPromisifiedStorage(),
rest: new RestClient("http://localhost/api"),
postMessage: new PostMessageService(),
browserInfo: { ios: false },
uuidGenerator: createUUIDGenerator(),
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
clearSession: () => Promise.resolve(),
};
const testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>
<AppQuery />
</TalkContextProvider>
);
return { context, testRenderer };
export default function create(params: CreateTestRendererParams) {
return createTestRenderer("auth", <AppQuery />, params);
}
@@ -1,30 +0,0 @@
import { IResolvers } from "graphql-tools";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import { createRelayEnvironment } from "talk-framework/testHelpers";
interface CreateEnvironmentParams {
logNetwork?: boolean;
muteNetworkErrors?: boolean;
resolvers?: IResolvers<any, any>;
initLocalState?: (
local: RecordProxy,
source: RecordSourceProxy,
environment: Environment
) => void;
}
export default function createEnvironment(params: CreateEnvironmentParams) {
return createRelayEnvironment({
network: {
logNetwork: params.logNetwork,
muteNetworkErrors: params.muteNetworkErrors,
resolvers: params.resolvers || {},
projectName: "tenant",
},
initLocalState: (localRecord, source, environment) => {
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
}
@@ -1,10 +0,0 @@
import path from "path";
import { createFluentBundle } from "talk-framework/testHelpers";
export default function create() {
return createFluentBundle(
"auth",
path.resolve(__dirname, "../../../../locales/en-US")
);
}
@@ -1,6 +1,7 @@
import { get, merge } from "lodash";
import { get } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
toJSON,
wait,
@@ -24,7 +25,7 @@ async function createTestRenderer(
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
.returns(pureMerge(settings, get(customResolver, "Query.settings"))),
},
};
@@ -1,6 +1,7 @@
import { get, merge } from "lodash";
import { get } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
toJSON,
wait,
@@ -24,7 +25,7 @@ async function createTestRenderer(
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
.returns(pureMerge(settings, get(customResolver, "Query.settings"))),
},
};
+3 -2
View File
@@ -1,6 +1,7 @@
import { get, merge } from "lodash";
import { get } from "lodash";
import sinon from "sinon";
import { pureMerge } from "talk-common/utils";
import {
toJSON,
wait,
@@ -24,7 +25,7 @@ async function createTestRenderer(
...customResolver.Query,
settings: sinon
.stub()
.returns(merge({}, settings, get(customResolver, "Query.settings"))),
.returns(pureMerge(settings, get(customResolver, "Query.settings"))),
},
};
@@ -82,12 +82,11 @@ const ForgotPassword: StatelessComponent<ForgotPasswordForm> = props => {
disabled={submitting}
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage>
{meta.error || meta.submitError}
</ValidationMessage>
)}
</FormField>
)}
</Field>
@@ -67,12 +67,11 @@ const SignInWithEmail: StatelessComponent<SignInWithEmailForm> = props => {
fullWidth
/>
</Localized>
{meta.touched &&
(meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
{meta.touched && (meta.error || meta.submitError) && (
<ValidationMessage fullWidth>
{meta.error || meta.submitError}
</ValidationMessage>
)}
<Flex justifyContent="flex-end">
<Localized id="signIn-forgotYourPassword">
<Button
@@ -5,7 +5,7 @@ import { createInMemoryStorage } from "talk-framework/lib/storage";
import withPymStorage from "./withPymStorage";
class PymStub {
public listeners: Record<string, ((msg: string) => void)> = {};
public listeners: Record<string, (msg: string) => void> = {};
public messages: Array<{ key: string; value: string }> = [];
public type: string;
@@ -114,8 +114,8 @@ function valueToState(
// Start from the first unit,
// keep first unit if value is set to 0,
// otherwise use better matching unit if the value is fully dividable by the unit.
unit = units.reduce(
(x, cur) => (parsed % cur === 0 && parsed !== 0 ? cur : x)
unit = units.reduce((x, cur) =>
parsed % cur === 0 && parsed !== 0 ? cur : x
);
}
// Compute new value relative to the selected unit.
@@ -6,9 +6,9 @@ import { PasswordField as PasswordFieldUI } from "talk-ui/components";
export interface Props
extends Omit<
PropTypesOf<typeof PasswordFieldUI>,
"showPasswordTitle" | "hidePasswordTitle"
> {}
PropTypesOf<typeof PasswordFieldUI>,
"showPasswordTitle" | "hidePasswordTitle"
> {}
const PasswordField: StatelessComponent<Props> = props => (
<Localized
@@ -26,10 +26,10 @@ import { PostMessageService } from "../postMessage";
import SendPymReady from "./SendPymReady";
import { TalkContext, TalkContextProvider } from "./TalkContext";
export type InitLocalState = ((
export type InitLocalState = (
environment: Environment,
context: TalkContext
) => void | Promise<void>);
) => void | Promise<void>;
interface CreateContextArguments {
/** Locales that the user accepts, usually `navigator.languages`. */
@@ -104,7 +104,7 @@ function createRelayEnvironment() {
return { environment, tokenGetter };
}
function createRestAPI(tokenGetter: (() => string)) {
function createRestAPI(tokenGetter: () => string) {
return new RestClient("/api", tokenGetter);
}
@@ -5,11 +5,7 @@ import React from "react";
interface Props {
form: FormApi;
rootKey?: string;
children: (
params: {
onInitValues: (data: any) => void;
}
) => React.ReactNode;
children: (params: { onInitValues: (data: any) => void }) => React.ReactNode;
}
/**
@@ -7,11 +7,9 @@ import { AddSubmitHook, SubmitHook, SubmitHookContextProvider } from "./";
interface Props {
onExecute: (data: any, form: FormApi) => Promise<void>;
children: (
params: {
onSubmit: (settings: any, form: FormApi) => void;
}
) => React.ReactNode;
children: (params: {
onSubmit: (settings: any, form: FormApi) => void;
}) => React.ReactNode;
}
/**
@@ -3,7 +3,7 @@ export interface BundledLocales {
}
export interface LoadableLocales {
[locale: string]: (() => Promise<string>);
[locale: string]: () => Promise<string>;
}
/**
@@ -15,6 +15,8 @@ import { TalkContext, withContext } from "../bootstrap";
* and the signature (input: I) => Promise<R>. Calling
* this will call the specified `commit` callback with
* the Relay `environment` provided by the context.
*
* @deprecated
*/
function createMutationContainer<T extends string, I, R>(
propName: T,
@@ -9,8 +9,8 @@ type RecordSourceProxy<T> = T extends object
readonly [P in keyof T]?: T[P] extends Array<infer U>
? ReadonlyArray<RecordSourceProxy<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<RecordSourceProxy<V>>
: RecordSourceProxy<T[P]>
? ReadonlyArray<RecordSourceProxy<V>>
: RecordSourceProxy<T[P]>
}
: T;
@@ -35,14 +35,18 @@ export type MutationProp<
> = T extends Mutation<any, infer I, infer R>
? Parameters<T["commit"]>[1] extends undefined
? () => R
: keyof Parameters<T["commit"]>[1] extends never ? () => R : (input: I) => R
: keyof Parameters<T["commit"]>[1] extends never
? () => R
: (input: I) => R
: never;
type RemoveClientMutationID<T> = T extends Promise<infer U>
? Promise<
U extends { clientMutationId: any } ? Omit<U, "clientMutationId"> : U
>
: T extends { clientMutationId: any } ? Omit<T, "clientMutationId"> : T;
: T extends { clientMutationId: any }
? Omit<T, "clientMutationId">
: T;
export function createMutation<N extends string, I, R>(
name: N,
@@ -10,21 +10,18 @@ export default function useLoadMore(
count: number
): [() => void, boolean] {
const [isLoadingMore, setIsLoadingMore] = useState(false);
const loadMore = useCallback(
() => {
if (!relay.hasMore() || relay.isLoading()) {
return;
const loadMore = useCallback(() => {
if (!relay.hasMore() || relay.isLoading()) {
return;
}
setIsLoadingMore(true);
relay.loadMore(count, error => {
setIsLoadingMore(false);
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
setIsLoadingMore(true);
relay.loadMore(count, error => {
setIsLoadingMore(false);
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
});
},
[relay]
);
});
}, [relay]);
return [loadMore, isLoadingMore];
}
@@ -15,34 +15,31 @@ export default function useRefetch<V = Variables>(
): [() => void, boolean] {
const [manualRefetchCount, setManualRefetchCount] = useState(0);
const [refetching, setRefetching] = useState(false);
useEffectAfterMount(
() => {
setRefetching(true);
const disposable = relay.refetchConnection(
10,
error => {
setRefetching(false);
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
},
variables
);
return () => {
if (disposable) {
disposable.dispose();
useEffectAfterMount(() => {
setRefetching(true);
const disposable = relay.refetchConnection(
10,
error => {
setRefetching(false);
if (error) {
// tslint:disable-next-line:no-console
console.error(error);
}
};
},
[
relay,
manualRefetchCount,
...Object.keys(variables).reduce<any[]>((a, k) => {
a.push((variables as any)[k]);
return a;
}, []),
]
);
},
variables
);
return () => {
if (disposable) {
disposable.dispose();
}
};
}, [
relay,
manualRefetchCount,
...Object.keys(variables).reduce<any[]>((a, k) => {
a.push((variables as any)[k]);
return a;
}, []),
]);
return [() => setManualRefetchCount(manualRefetchCount + 1), refetching];
}
@@ -1,7 +1,7 @@
import createPymStorage from "./PymStorage";
class PymStub {
public listeners: Record<string, ((msg: string) => void)> = {};
public listeners: Record<string, (msg: string) => void> = {};
public messages: Array<{ key: string; value: string }> = [];
public type: string;
@@ -14,7 +14,7 @@ class PymStorage implements PromisifiedStorage {
/** A Map of requestID => {resolve, reject} */
private requests: Record<
string,
{ resolve: ((v: any) => void); reject: ((v: any) => void) }
{ resolve: (v: any) => void; reject: (v: any) => void }
> = {};
/** Requests method with parameters over pym. */
@@ -1,24 +1,46 @@
import { merge } from "lodash";
import { pureMerge } from "talk-common/utils";
import { DeepPartial } from "talk-framework/types";
/**
* Fixture prepares schema type to be used in fixtures.
* It adds an optional `__typename` to the schema type and
* marks fields as optional.
* Callbackify turns Fields e.g. `{a: string}`
* to also allow a callback `{a: string | () => string}`
*/
export type Fixture<T> = T extends object
export type Callbackify<T> = T extends object
?
| {
[P in keyof T]: T[P] extends Array<infer U>
? Array<Callbackify<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<Callbackify<V>>
: Callbackify<T[P]>
}
| (() => {
[P in keyof T]: T[P] extends Array<infer U>
? Array<Callbackify<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<Callbackify<V>>
: Callbackify<T[P]>
})
: T | (() => T);
/**
* WithTypename adds `__typename` to allowed props deeply.
*/
export type WithTypename<T> = T extends object
? {
// (cvle): We don't use & { __typename?: string } because for some reason
// typescript would allow field names that are not defined!
[P in keyof T | "__typename"]?: P extends keyof T
? T[P] extends Array<infer U>
? Array<Fixture<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<Fixture<V>>
: Fixture<T[P]>
: string
}
[P in keyof T]: T[P] extends Array<infer U>
? Array<WithTypename<U>>
: T[P] extends ReadonlyArray<infer V>
? ReadonlyArray<WithTypename<V>>
: WithTypename<T[P]>
} & { __typename?: string }
: T;
/**
* Fixture adds typenames and is deeply partial.
*/
export type Fixture<T> = DeepPartial<WithTypename<T>>;
/**
* createFixture lets you input the data of a schema object as deep partial
* including it's `__typename`, merged it with `base` and return it as the
@@ -26,9 +48,12 @@ export type Fixture<T> = T extends object
* to only include fields that exists in `data` and `base` though to
* type this it seems we need partial generic inferation support.
*/
export default function createFixture<T>(data: Fixture<T>, base?: T): T {
export default function createFixture<T>(
data: Fixture<T>,
base?: T
): WithTypename<T> {
if (base) {
return merge({}, base, data);
return pureMerge(base, data) as any;
}
return data as T;
return data as any;
}
@@ -1,4 +1,4 @@
import createFixture, { Fixture } from "./createFixture";
import createFixture, { Fixture, WithTypename } from "./createFixture";
/**
* createFixtures lets you input an array of data of a schema object as deep partial
@@ -10,6 +10,6 @@ import createFixture, { Fixture } from "./createFixture";
export default function createFixtures<T>(
data: Array<Fixture<T>>,
base?: T
): T[] {
return data.map(d => createFixture(d, base)) as T[];
): Array<WithTypename<T>> {
return data.map(d => createFixture(d, base)) as any;
}
@@ -1,16 +1,36 @@
import { identity, omit } from "lodash";
import sinon from "sinon";
import { Mutation } from "talk-framework/lib/relay/mutation";
import { Omit } from "talk-framework/types";
import { Fixture } from "./createFixture";
import { Resolver } from "./createTestRenderer";
export type NoClientMutationID<T> = T extends { clientMutationId: any }
? Omit<T, "clientMutationId">
: T;
export type MutationResult<T> = NoClientMutationID<Fixture<T>>;
export type MutationResultVariations<T> = T extends Resolver<any, infer R>
? MutationResult<R>
: never;
export type MutationResolverCallback<T extends Resolver<any, any>> = (data: {
variables: T extends Resolver<infer V, any>
? V extends { input: infer W }
? NoClientMutationID<W>
: never
: never;
callCount: number;
typecheck: (data: MutationResultVariations<T>) => MutationResultVariations<T>;
}) => MutationResultVariations<T>;
/**
* createMutationResolverStub makes it easier to write a SinonStub.
* Given a `ResolverType` from the Schema it'll provide types as well!.
*/
export default function createMutationResolverStub<
T extends Mutation<any, any, any>
>(
callback: (
variables: T extends Mutation<any, infer I, any> ? I : never,
callCount: number
) => T extends Mutation<any, any, infer R>
? R extends Promise<infer U> ? U | R : R | Promise<R>
: never
) {
T extends Resolver<any, any>
>(callback: MutationResolverCallback<T>) {
let callCount = 0;
const lastClientMutationIds: any[] = [];
const resolver = async (_: any, data: any) => {
@@ -18,7 +38,11 @@ export default function createMutationResolverStub<
expectAndFail(clientMutationId).toBeTruthy();
expectAndFail(lastClientMutationIds).not.toContain(clientMutationId);
lastClientMutationIds.push(clientMutationId);
const result = await callback(data.input, callCount++);
const result = await callback({
variables: omit(data.input, "clientMutationId"),
callCount: callCount++,
typecheck: identity,
});
expectAndFail(result.clientMutationId).toBeUndefined();
result.clientMutationId = clientMutationId;
return result;
@@ -1,17 +1,33 @@
import { identity } from "lodash";
import sinon from "sinon";
type Resolver<V, R> = (parent: any, args: V, context: any, info: any) => R;
import { Fixture } from "./createFixture";
import { Resolver } from "./createTestRenderer";
export type QueryResult<T> = Fixture<T>;
export type QueryResultVariations<
T extends Resolver<any, any>
> = T extends Resolver<any, infer R> ? QueryResult<R> : never;
export type QueryResolverCallback<T extends Resolver<any, any>> = (data: {
variables: T extends Resolver<infer V, any> ? V : never;
callCount: number;
typecheck: (data: QueryResultVariations<T>) => QueryResultVariations<T>;
}) => QueryResultVariations<T>;
/**
* createQueryResolverStub makes it easier to write a SinonStub.
* Given a `ResolverType` from the Schema it'll provide types as well!.
*/
export default function createQueryResolverStub<T extends Resolver<any, any>>(
callback: (
variables: T extends Resolver<infer V, any> ? V : never,
callCount: number
) => T extends Resolver<any, infer R>
? R extends Promise<infer U> ? U | R : R | Promise<R>
: never
callback: QueryResolverCallback<T>
) {
let callCount = 0;
return sinon
.stub()
.callsFake((_: any, data: any) => callback(data, callCount++));
return sinon.stub().callsFake((fallback: any, variables: any) => {
return callback({
variables: variables || fallback,
callCount: callCount++,
typecheck: identity,
});
});
}
@@ -28,7 +28,7 @@ export interface CreateRelayEnvironmentNetworkParams {
/** project name of graphql-config */
projectName: string;
/** graphql resolvers */
resolvers: IResolvers<any, any>;
resolvers?: IResolvers<any, any>;
/** If enabled, graphql responses will be logged to the console */
logNetwork?: boolean;
/** If enabled, graphql errors will be muted */
@@ -99,7 +99,7 @@ export default function createRelayEnvironment(
if (params.network) {
const schema = loadSchema(
params.network.projectName,
params.network.resolvers,
params.network.resolvers || {},
{ requireResolversForResolveType: false }
);
network = Network.create(
@@ -0,0 +1,84 @@
import { mapValues } from "lodash";
import { SinonStub } from "sinon";
import createMutationResolverStub, {
MutationResolverCallback,
} from "./createMutationResolverStub";
import createQueryResolverStub, {
QueryResolverCallback,
} from "./createQueryResolverStub";
import { Resolvers } from "./createTestRenderer";
export interface ResolversTemplate<T extends Resolvers = any> {
Query?: {
[P in keyof Required<T>["Query"]]: QueryResolverCallback<
Required<T>["Query"][P]
>
};
Mutation?: {
[P in keyof Required<T>["Mutation"]]: MutationResolverCallback<
Required<T>["Mutation"][P]
>
};
}
export interface ResolversStub<T extends Resolvers = any> {
Query?: { [P in keyof Required<T>["Query"]]: SinonStub };
Mutation?: { [P in keyof Required<T>["Mutation"]]: SinonStub };
}
function isSinonStub(v: any) {
return v.called !== undefined;
}
/**
* createResolversStub is a helper for creating resolvers.
*
* Instead of writing:
* ```ts
* {
* Query: {
* settings: createQueryResolverStub<QueryToSettingsResolver>(({ variables }) => settings)
* users: createQueryResolverStub<QueryToUsersResolver>(() => users)
* viewer: createQueryResolverStub<QueryToSettingsResolver>(() => viewer)
* },
* Mutation: {
* // Same goes for Mutations
* }
* },
* ```
*
* You can do
* ```ts
* createResolversStub<GQLResolver>({
* Query: {
* settings: ({ variables }) => settings
* users: () => users
* viewer: () => viewer
* },
* Mutation: {
* // Same goes for Mutations
* }
* }),
* ```
*/
export default function createResolversStub<T extends Resolvers = any>(
resolvers: ResolversTemplate<T>
): ResolversStub<T> {
const result: any = {};
if (resolvers.Query) {
result.Query = mapValues(resolvers.Query, v =>
typeof v === "function" && !isSinonStub(v)
? createQueryResolverStub(v)
: v
);
}
if (resolvers.Mutation) {
result.Mutation = mapValues(resolvers.Mutation, v =>
typeof v === "function" && !isSinonStub(v)
? createMutationResolverStub(v)
: v
);
}
return result;
}
@@ -0,0 +1,107 @@
import { EventEmitter2 } from "eventemitter2";
import { IResolvers } from "graphql-tools";
import { noop } from "lodash";
import path from "path";
import React from "react";
import TestRenderer, { ReactTestRenderer } from "react-test-renderer";
import { Environment, RecordProxy, RecordSourceProxy } from "relay-runtime";
import { TalkContext, TalkContextProvider } from "talk-framework/lib/bootstrap";
import { PostMessageService } from "talk-framework/lib/postMessage";
import { RestClient } from "talk-framework/lib/rest";
import { createPromisifiedStorage } from "talk-framework/lib/storage";
import { createUUIDGenerator } from "talk-framework/testHelpers";
import createFluentBundle from "./createFluentBundle";
import createRelayEnvironment from "./createRelayEnvironment";
export type Resolver<V, R> = (
parent: any,
args: V,
context: any,
info: any
) => R;
export interface Resolvers<Q extends Resolver<any, any> = any, M = any> {
Query?: Q;
Mutation?: M;
}
export interface TestResolvers<T extends Resolvers = any> {
Query?: { [P in keyof Required<T>["Query"]]: (() => any) };
Mutation?: { [P in keyof Required<T>["Mutation"]]: (() => any) };
}
function createNodeMock(element: React.ReactElement<any>) {
if (element.type === "div") {
return {
innerHtml: "",
className: "",
focus: noop,
};
}
return null;
}
export interface CreateTestRendererParams<T extends Resolvers = any> {
logNetwork?: boolean;
muteNetworkErrors?: boolean;
resolvers?: TestResolvers<T>;
browserInfo?: TalkContext["browserInfo"];
initLocalState?: (
local: RecordProxy,
source: RecordSourceProxy,
environment: Environment
) => void;
}
export default function createTestRenderer<
T extends { Query?: any; Mutation?: any } = any
>(
target: string,
element: React.ReactNode,
params: CreateTestRendererParams<T>
) {
const environment = createRelayEnvironment({
network: {
// Set this to true, to see graphql responses.
logNetwork: params.logNetwork,
resolvers: params.resolvers as IResolvers<any, any>,
muteNetworkErrors: params.muteNetworkErrors,
projectName: "tenant",
},
initLocalState: (localRecord, source, env) => {
if (params.initLocalState) {
params.initLocalState(localRecord, source, env);
}
},
});
const context: TalkContext = {
relayEnvironment: environment,
locales: ["en-US"],
localeBundles: [
createFluentBundle(
target,
path.resolve(__dirname, "../../../../locales/en-US")
),
],
localStorage: createPromisifiedStorage(),
sessionStorage: createPromisifiedStorage(),
rest: new RestClient("http://localhost/api"),
postMessage: new PostMessageService(),
browserInfo: params.browserInfo || { ios: false },
uuidGenerator: createUUIDGenerator(),
eventEmitter: new EventEmitter2({ wildcard: true, maxListeners: 20 }),
clearSession: () => Promise.resolve(),
};
let testRenderer: ReactTestRenderer;
TestRenderer.act(() => {
testRenderer = TestRenderer.create(
<TalkContextProvider value={context}>{element}</TalkContextProvider>,
{ createNodeMock }
);
});
return { context, testRenderer: testRenderer! };
}
+10 -2
View File
@@ -11,7 +11,6 @@ export {
export { default as createUUIDGenerator } from "./createUUIDGenerator";
export * from "./denormalize";
export { default as limitSnapshotTo } from "./limitSnapshotTo";
export { default as inputPredicate } from "./inputPredicate";
export { default as within } from "./within";
export { default as wait } from "./wait";
export { default as waitForElement } from "./waitForElement";
@@ -22,9 +21,18 @@ export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
export { default as createAccessToken } from "./createAccessToken";
export { default as findParentsWithType } from "./findParentsWithType";
export { default as findParentWithType } from "./findParentWithType";
export { default as createFixture } from "./createFixture";
export {
default as createFixture,
Fixture,
WithTypename,
} from "./createFixture";
export { default as createFixtures } from "./createFixtures";
export {
default as createMutationResolverStub,
} from "./createMutationResolverStub";
export { default as createQueryResolverStub } from "./createQueryResolverStub";
export {
default as createTestRenderer,
CreateTestRendererParams,
} from "./createTestRenderer";
export { default as createResolversStub } from "./createResolversStub";
@@ -1,10 +0,0 @@
import { ReactTestInstance } from "react-test-renderer";
const inputPredicate = (nameOrID: string) => (n: ReactTestInstance) => {
return (
[n.props.name, n.props.id].indexOf(nameOrID) > -1 &&
["input", "button"].indexOf(n.type as string) > -1
);
};
export default inputPredicate;
@@ -12,8 +12,8 @@ export type NoFragmentRefs<T> = T extends object
? T extends ((...args: any[]) => any)
? T
: T extends ReadonlyArray<infer U>
? ReadonlyArray<NoFragmentRefs2<U>> // TODO: (cvle) this should normally reference itself but it complains about a circular reference.
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
? ReadonlyArray<NoFragmentRefs2<U>> // TODO: (cvle) this should normally reference itself but it complains about a circular reference.
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
: T;
// TODO: (cvle) these NoFragmentRefX are a workaround for above issue
@@ -21,16 +21,16 @@ export type NoFragmentRefs2<T> = T extends object
? T extends ((...args: any[]) => any)
? T
: T extends ReadonlyArray<infer U>
? ReadonlyArray<NoFragmentRefs3<U>>
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
? ReadonlyArray<NoFragmentRefs3<U>>
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
: T;
export type NoFragmentRefs3<T> = T extends object
? T extends ((...args: any[]) => any)
? T
: T extends ReadonlyArray<infer U>
? ReadonlyArray<NoFragmentRefs4<U>>
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
? ReadonlyArray<NoFragmentRefs4<U>>
: { [P in keyof OmitFragments<T>]: NoFragmentRefs<T[P]> }
: T;
export type NoFragmentRefs4<T> = T extends object

Some files were not shown because too many files have changed in this diff Show More