mirror of
https://github.com/wassname/talk.git
synced 2026-07-09 21:36:00 +08:00
[next] Auth Popup v2 (#2101)
* feat: Implement new Sign In view * feat: Move forgot + resetPassword to new design * feat: Implement sign up with new design * fix: narrow gutter * test: add unit tests * test: integration tests * feat: support show / hide password * feat: support oauth2 flow * feat: add views for user completion * feat: implement oauth2 sign up * test: fix snapshots * fix: lint * fix: get more complete mutation response * fix: removed array of OIDC integrations * fix: renamed resolver function * fix: adapt oidc client implementation * fix: targetFilter should be stream on signup * fix: removed unneeded message * fix: moved password into local profile * fix: made username optional, removed valid null value * fix: linting * fix: respect targetFilter * feat: support user registration mutations - Added `setUsername` - Added `setEmail` - Added `setPassword` - Added `permit` to `@auth` - Added `email` to `User` * fix: fixed issue with query * feat: added user password update * feat: complete sign in mutation * fix: adapt some rebasing gitches * test: improve tests * test: unittest for setting auth token * fix: failing tests * test: move most tests from enzyme to react-test-renderer * fix: remove schema warnings in tests * test: improve window mock * test: test different social login configurations * test: test social logins for sign up * fix: use htmlFor instead of for * test: more feature tests * feat: always go through account completion * test: feature test account completion * feat: addtional account completion test * Update start.ts * chore: refactor auth token retrieval logic
This commit is contained in:
+2
-2
@@ -13,7 +13,7 @@ interface State {
|
||||
copied: boolean;
|
||||
}
|
||||
|
||||
class PermalinkPopover extends React.Component<InnerProps> {
|
||||
class CopyButton extends React.Component<InnerProps> {
|
||||
private timeout: any = null;
|
||||
|
||||
public state: State = {
|
||||
@@ -59,4 +59,4 @@ class PermalinkPopover extends React.Component<InnerProps> {
|
||||
}
|
||||
}
|
||||
|
||||
export default PermalinkPopover;
|
||||
export default CopyButton;
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "./CopyButton";
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Localized } from "fluent-react/compat";
|
||||
import React, { StatelessComponent } from "react";
|
||||
|
||||
import { Omit, PropTypesOf } from "talk-framework/types";
|
||||
import { PasswordField as PasswordFieldUI } from "talk-ui/components";
|
||||
|
||||
export interface Props
|
||||
extends Omit<
|
||||
PropTypesOf<typeof PasswordFieldUI>,
|
||||
"showPasswordTitle" | "hidePasswordTitle"
|
||||
> {}
|
||||
|
||||
const PasswordField: StatelessComponent<Props> = props => (
|
||||
<Localized
|
||||
id="framework-passwordField"
|
||||
attrs={{ showPasswordTitle: true, hidePasswordTitle: true }}
|
||||
>
|
||||
<PasswordFieldUI {...props} />
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export default PasswordField;
|
||||
@@ -1 +1,2 @@
|
||||
export { default as CopyButton } from "./CopyButton";
|
||||
export { default as PasswordField } from "./PasswordField";
|
||||
|
||||
@@ -54,6 +54,12 @@ export const PASSWORDS_DO_NOT_MATCH = () => (
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const EMAILS_DO_NOT_MATCH = () => (
|
||||
<Localized id="framework-validation-emailsDoNotMatch">
|
||||
<span>Emails do not match. Try again.</span>
|
||||
</Localized>
|
||||
);
|
||||
|
||||
export const INVALID_URL = () => (
|
||||
<Localized id="framework-validation-invalidURL">
|
||||
<span>Invalid URL</span>
|
||||
|
||||
@@ -6,19 +6,21 @@ import { FetchFunction } from "relay-runtime";
|
||||
*/
|
||||
export default function wrapFetchWithLogger(
|
||||
fetch: FetchFunction,
|
||||
logResult?: boolean
|
||||
options: { logResult?: boolean; muteErrors?: boolean } = {}
|
||||
): FetchFunction {
|
||||
return async (...args: any[]) => {
|
||||
try {
|
||||
const result = await (fetch as any)(...args);
|
||||
if (logResult) {
|
||||
if (options.logResult) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.log(JSON.stringify(result));
|
||||
}
|
||||
return result;
|
||||
} catch (err) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
if (!options.muteErrors) {
|
||||
// tslint:disable-next-line:no-console
|
||||
console.error(err);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ReactNode } from "react";
|
||||
import {
|
||||
EMAILS_DO_NOT_MATCH,
|
||||
INVALID_CHARACTERS,
|
||||
INVALID_EMAIL,
|
||||
INVALID_URL,
|
||||
@@ -101,10 +102,18 @@ export const validatePassword = createValidator(
|
||||
PASSWORD_TOO_SHORT(8)
|
||||
);
|
||||
|
||||
/**s
|
||||
* validateUsername is a Validator that checks that the value is a valid username.
|
||||
/**
|
||||
* validateEqualPasswords is a Validator that checks for correct password confirmation.
|
||||
*/
|
||||
export const validateEqualPasswords = createValidator(
|
||||
(v, values) => v === values.password,
|
||||
PASSWORDS_DO_NOT_MATCH()
|
||||
);
|
||||
|
||||
/**
|
||||
* validateEqualEmails is a Validator that checks for correct email confirmation.
|
||||
*/
|
||||
export const validateEqualEmails = createValidator(
|
||||
(v, values) => v === values.email,
|
||||
EMAILS_DO_NOT_MATCH()
|
||||
);
|
||||
|
||||
@@ -4,7 +4,10 @@ import sinon from "sinon";
|
||||
import { TalkContext } from "talk-framework/lib/bootstrap";
|
||||
import { LOCAL_ID } from "talk-framework/lib/relay";
|
||||
import { createPromisifiedStorage } from "talk-framework/lib/storage";
|
||||
import { createRelayEnvironment } from "talk-framework/testHelpers";
|
||||
import {
|
||||
createAuthToken,
|
||||
createRelayEnvironment,
|
||||
} from "talk-framework/testHelpers";
|
||||
|
||||
import { commit } from "./SetAuthTokenMutation";
|
||||
|
||||
@@ -17,17 +20,7 @@ beforeAll(() => {
|
||||
});
|
||||
});
|
||||
|
||||
const authToken = `${btoa(
|
||||
JSON.stringify({
|
||||
alg: "HS256",
|
||||
typ: "JWT",
|
||||
})
|
||||
)}.${btoa(
|
||||
JSON.stringify({
|
||||
exp: 1540503165,
|
||||
jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce",
|
||||
})
|
||||
)}`;
|
||||
const authToken = createAuthToken();
|
||||
|
||||
it("Sets auth token to localStorage", async () => {
|
||||
const clearSessionStub = sinon.stub();
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { ReactTestInstance } from "react-test-renderer";
|
||||
|
||||
import { queryAllByText } from "./byText";
|
||||
import matchText, { TextMatchOptions, TextMatchPattern } from "./matchText";
|
||||
|
||||
const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
|
||||
i: ReactTestInstance
|
||||
) => {
|
||||
const ariaLabelMatcher = (
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) => (i: ReactTestInstance) => {
|
||||
// Only look at dom components.
|
||||
if (typeof i.type !== "string" || !i.props["aria-label"]) {
|
||||
return false;
|
||||
@@ -20,31 +22,14 @@ export function getByLabelText(
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
return container.find(matcher(pattern, options));
|
||||
}
|
||||
|
||||
export function queryByLabelText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
try {
|
||||
return container.find(matcher(pattern, options));
|
||||
} catch {
|
||||
return null;
|
||||
const results = queryAllByLabelText(container, pattern, options);
|
||||
if (results.length === 1) {
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function queryAllByLabelText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
try {
|
||||
return container.findAll(matcher(pattern, options));
|
||||
} catch {
|
||||
return [];
|
||||
if (results.length === 0) {
|
||||
throw new Error(`Could't find element with label text ${pattern}`);
|
||||
}
|
||||
throw new Error(`Found multiple elements with label text ${pattern}`);
|
||||
}
|
||||
|
||||
export function getAllByLabelText(
|
||||
@@ -52,5 +37,55 @@ export function getAllByLabelText(
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
return container.findAll(matcher(pattern, options));
|
||||
const results = queryAllByLabelText(container, pattern, options);
|
||||
if (results.length) {
|
||||
return results;
|
||||
}
|
||||
throw new Error(`Could't find element with label text ${pattern}`);
|
||||
}
|
||||
|
||||
export function queryByLabelText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
const results = queryAllByLabelText(container, pattern, options);
|
||||
if (results.length) {
|
||||
return results[0];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function queryAllByLabelText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
const matches = container.findAll(ariaLabelMatcher(pattern, options));
|
||||
queryAllByText(container, pattern).forEach(i => {
|
||||
if (typeof i.type !== "string") {
|
||||
return;
|
||||
}
|
||||
if (i.props.id) {
|
||||
try {
|
||||
matches.push(
|
||||
container.find(
|
||||
x =>
|
||||
typeof x.type === "string" &&
|
||||
x.props["aria-labelledby"] === i.props.id
|
||||
)
|
||||
);
|
||||
} catch {} // tslint:disable-line:no-empty
|
||||
}
|
||||
if (i.type === "label" && i.props.htmlFor) {
|
||||
try {
|
||||
matches.push(
|
||||
container.find(
|
||||
x => typeof x.type === "string" && x.props.id === i.props.htmlFor
|
||||
)
|
||||
);
|
||||
} catch {} // tslint:disable-line:no-empty
|
||||
}
|
||||
});
|
||||
return matches;
|
||||
}
|
||||
|
||||
@@ -6,10 +6,10 @@ const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
|
||||
i: ReactTestInstance
|
||||
) => {
|
||||
// Only look at dom components.
|
||||
if (typeof i.type !== "string" || !i.props["data-test"]) {
|
||||
if (typeof i.type !== "string" || !i.props["data-testid"]) {
|
||||
return false;
|
||||
}
|
||||
return matchText(pattern, i.props["data-test"], {
|
||||
return matchText(pattern, i.props["data-testid"], {
|
||||
collapseWhitespace: false,
|
||||
...options,
|
||||
});
|
||||
@@ -23,34 +23,34 @@ export function getByTestID(
|
||||
return container.find(matcher(pattern, options));
|
||||
}
|
||||
|
||||
export function getAllByTestID(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
const results = container.findAll(matcher(pattern, options));
|
||||
if (!results.length) {
|
||||
throw new Error(`Couldn't find test id ${pattern}`);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function queryByTestID(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
try {
|
||||
return container.find(matcher(pattern, options));
|
||||
} catch {
|
||||
const results = container.findAll(matcher(pattern, options));
|
||||
if (!results.length) {
|
||||
return null;
|
||||
}
|
||||
return results[0];
|
||||
}
|
||||
|
||||
export function queryAllByTestID(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
try {
|
||||
return container.findAll(matcher(pattern, options));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllByTestID(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
return container.findAll(matcher(pattern, options));
|
||||
}
|
||||
|
||||
@@ -30,34 +30,34 @@ export function getByText(
|
||||
return container.find(matcher(pattern, options));
|
||||
}
|
||||
|
||||
export function getAllByText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
const results = container.findAll(matcher(pattern, options));
|
||||
if (!results.length) {
|
||||
throw new Error(`Couldn't find text ${pattern}`);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function queryByText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
try {
|
||||
return container.find(matcher(pattern, options));
|
||||
} catch {
|
||||
const results = container.findAll(matcher(pattern, options));
|
||||
if (!results.length) {
|
||||
return null;
|
||||
}
|
||||
return results[0];
|
||||
}
|
||||
|
||||
export function queryAllByText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
try {
|
||||
return container.findAll(matcher(pattern, options));
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getAllByText(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
return container.findAll(matcher(pattern, options));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ReactTestInstance } from "react-test-renderer";
|
||||
|
||||
import matchText, { TextMatchOptions, TextMatchPattern } from "./matchText";
|
||||
|
||||
const matcher = (pattern: TextMatchPattern, options?: TextMatchOptions) => (
|
||||
i: ReactTestInstance
|
||||
) => {
|
||||
// Only look at dom components.
|
||||
if (typeof i.type !== "string") {
|
||||
return false;
|
||||
}
|
||||
return matchText(pattern, i.type, {
|
||||
collapseWhitespace: false,
|
||||
...options,
|
||||
});
|
||||
};
|
||||
|
||||
export function getByType(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
return container.find(matcher(pattern, options));
|
||||
}
|
||||
|
||||
export function getAllByType(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
const results = container.findAll(matcher(pattern, options));
|
||||
if (!results.length) {
|
||||
throw new Error(`Couldn't find test id ${pattern}`);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export function queryByType(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
const results = container.findAll(matcher(pattern, options));
|
||||
if (!results.length) {
|
||||
return null;
|
||||
}
|
||||
return results[0];
|
||||
}
|
||||
|
||||
export function queryAllByType(
|
||||
container: ReactTestInstance,
|
||||
pattern: TextMatchPattern,
|
||||
options?: TextMatchOptions
|
||||
) {
|
||||
return container.findAll(matcher(pattern, options));
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
export default function createAuthToken() {
|
||||
return `${btoa(
|
||||
JSON.stringify({
|
||||
alg: "HS256",
|
||||
typ: "JWT",
|
||||
})
|
||||
)}.${btoa(
|
||||
JSON.stringify({
|
||||
jti: "31b26591-4e9a-4388-a7ff-e1bdc5d97cce",
|
||||
})
|
||||
)}`;
|
||||
}
|
||||
@@ -26,6 +26,8 @@ export interface CreateRelayEnvironmentNetworkParams {
|
||||
resolvers: IResolvers<any, any>;
|
||||
/** If enabled, graphql responses will be logged to the console */
|
||||
logNetwork?: boolean;
|
||||
/** If enabled, graphql errors will be muted */
|
||||
muteNetworkErrors?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateRelayEnvironmentParams {
|
||||
@@ -58,10 +60,14 @@ export default function createRelayEnvironment(
|
||||
if (params.network) {
|
||||
const schema = loadSchema(
|
||||
params.network.projectName,
|
||||
params.network.resolvers
|
||||
params.network.resolvers,
|
||||
{ requireResolversForResolveType: false }
|
||||
);
|
||||
network = Network.create(
|
||||
wrapFetchWithLogger(createFetch({ schema }), params.network.logNetwork)
|
||||
wrapFetchWithLogger(createFetch({ schema }), {
|
||||
logResult: params.network.logNetwork,
|
||||
muteErrors: params.network.muteNetworkErrors,
|
||||
})
|
||||
);
|
||||
}
|
||||
const environment = new Environment({
|
||||
|
||||
@@ -10,25 +10,13 @@ export {
|
||||
} from "./removeFragmentRefs";
|
||||
export { default as createUUIDGenerator } from "./createUUIDGenerator";
|
||||
export * from "./denormalize";
|
||||
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
|
||||
export { default as limitSnapshotTo } from "./limitSnapshotTo";
|
||||
export { default as inputPredicate } from "./inputPredicate";
|
||||
export {
|
||||
getByTestID,
|
||||
getAllByTestID,
|
||||
queryByTestID,
|
||||
queryAllByTestID,
|
||||
} from "./byTestID";
|
||||
export { getByText, getAllByText, queryByText, queryAllByText } from "./byText";
|
||||
export {
|
||||
getByLabelText,
|
||||
getAllByLabelText,
|
||||
queryByLabelText,
|
||||
queryAllByLabelText,
|
||||
} from "./byLabelText";
|
||||
export { default as within } from "./within";
|
||||
export { default as wait } from "./wait";
|
||||
export { default as waitForElement } from "./waitForElement";
|
||||
export { default as waitUntilThrow } from "./waitUntilThrow";
|
||||
export { default as matchText } from "./matchText";
|
||||
export { default as toJSON } from "./toJSON";
|
||||
export { default as replaceHistoryLocation } from "./replaceHistoryLocation";
|
||||
export { default as createAuthToken } from "./createAuthToken";
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export default function limitSnapshotTo(dataTest: string, node: any) {
|
||||
if (node.props && node.props["data-test"] === dataTest) {
|
||||
if (node.props && node.props["data-testid"] === dataTest) {
|
||||
return node;
|
||||
}
|
||||
if (node.children) {
|
||||
|
||||
@@ -13,6 +13,12 @@ import {
|
||||
queryByTestID,
|
||||
} from "./byTestID";
|
||||
import { getAllByText, getByText, queryAllByText, queryByText } from "./byText";
|
||||
import {
|
||||
getAllByType,
|
||||
getByType,
|
||||
queryAllByType,
|
||||
queryByType,
|
||||
} from "./byType";
|
||||
import toJSON from "./toJSON";
|
||||
|
||||
type Func0<R> = () => R;
|
||||
@@ -49,6 +55,10 @@ export default function within(container: ReactTestInstance) {
|
||||
getAllByLabelText: applyContainer(container, getAllByLabelText),
|
||||
queryByLabelText: applyContainer(container, queryByLabelText),
|
||||
queryAllByLabelText: applyContainer(container, queryAllByLabelText),
|
||||
getByType: applyContainer(container, getByType),
|
||||
getAllByType: applyContainer(container, getAllByType),
|
||||
queryByType: applyContainer(container, queryByType),
|
||||
queryAllByType: applyContainer(container, queryAllByType),
|
||||
toJSON: () => toJSON(container),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user