[CORL-445] Change Password (#2426)

* fix: reversed `new-password` autocomplete option

* feat: initial implementation

* fix: localization and testing

* fix: updated snapshot
This commit is contained in:
Wyatt Johnson
2019-08-01 22:08:14 +00:00
committed by GitHub
parent 290ceee8e9
commit 836a2267bf
48 changed files with 1122 additions and 197 deletions
+13
View File
@@ -93,6 +93,19 @@ export const settings = createFixture<GQLSettings>({
},
});
export const settingsWithoutLocalAuth = createFixture<GQLSettings>(
{
auth: {
integrations: {
local: {
enabled: false,
},
},
},
},
settings
);
export const baseUser = createFixture<GQLUser>({
createdAt: "2018-02-06T18:24:00.000Z",
status: {
@@ -0,0 +1,337 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders the empty settings pane 1`] = `
<div
className="Box-root HorizontalGutter-root App-root HorizontalGutter-full"
>
<ul
className="TabBar-root TabBar-primary coral coral-tabBar"
role="tablist"
>
<li
className="Tab-root"
id="tab-COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-primary coral coral-tabBar-allComments"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
<span>
Comments
</span>
</button>
</li>
<li
className="Tab-root"
id="tab-PROFILE"
role="presentation"
>
<button
aria-controls="tabPane-PROFILE"
aria-selected={true}
className="BaseButton-root Tab-button Tab-primary Tab-active coral coral-tabBar-myProfile"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
<span>
My Profile
</span>
</button>
</li>
</ul>
<section
aria-labelledby="tab-PROFILE"
className="App-tabContent"
data-testid="current-tab-pane"
id="tabPane-PROFILE"
role="tabpanel"
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-5"
>
<div
className="Box-root Flex-root"
>
<div
className="Flex-flex Flex-halfItemGutter Flex-wrap gutter"
>
<div
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Signed in as
<span
className="Box-root Typography-root Typography-bodyCopyBold Typography-colorTextPrimary"
>
Passivo
</span>
.
</div>
<div
className="Box-root Flex-root Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary Flex-flex"
>
<span>
Not you? 
</span>
<button
className="BaseButton-root Button-root Button-sizeSmall Button-colorPrimary Button-variantUnderlined"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="button"
>
Sign Out
</button>
</div>
</div>
</div>
<ul
className="TabBar-root TabBar-secondary"
role="tablist"
>
<li
className="Tab-root"
id="tab-MY_COMMENTS"
role="presentation"
>
<button
aria-controls="tabPane-MY_COMMENTS"
aria-selected={false}
className="BaseButton-root Tab-button Tab-secondary"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
<span>
My Comments
</span>
</button>
</li>
<li
className="Tab-root"
id="tab-SETTINGS"
role="presentation"
>
<button
aria-controls="tabPane-SETTINGS"
aria-selected={true}
className="BaseButton-root Tab-button Tab-secondary Tab-active"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
role="tab"
type="button"
>
<span>
Settings
</span>
</button>
</li>
</ul>
<section
aria-labelledby="tab-SETTINGS"
id="tabPane-SETTINGS"
role="tabpanel"
>
<div
className="Box-root HorizontalGutter-root SettingsContainer-root HorizontalGutter-spacing-5"
>
<div
data-testid="profile-settings-ignoredCommenters"
>
<h1
className="Box-root Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Ignored Commenters
</h1>
<p
className="IgnoreUserSettingsContainer-description"
>
Once you ignore someone, all of their comments are hidden from you.
Commenters you ignore will still be able to see your comments.
</p>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-spacing-1"
>
<div
className="IgnoreUserSettingsContainer-empty"
>
You are not currently ignoring anyone
</div>
</div>
</div>
<div
data-testid="profile-settings-changePassword"
>
<form
autoComplete="off"
onSubmit={[Function]}
>
<div
className="Box-root HorizontalGutter-root HorizontalGutter-oneAndAHalf"
>
<h1
className="Box-root Typography-root Typography-heading3 Typography-colorTextPrimary"
>
Change Password
</h1>
<fieldset
className="FieldSet-root Box-root HorizontalGutter-root HorizontalGutter-full"
>
<fieldset
className="FieldSet-root Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="oldPassword"
>
Old Password
</label>
<div
className="PasswordField-fullWidth PasswordField-root"
>
<div
className="PasswordField-wrapper"
>
<input
autoCapitalize="off"
autoComplete="current-password"
autoCorrect="off"
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
disabled={false}
id="oldPassword"
name="oldPassword"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder=""
spellCheck={false}
type="password"
value=""
/>
<div
className="PasswordField-icon"
onClick={[Function]}
role="button"
tabIndex={0}
title="Hide password"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</div>
</div>
</div>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<p
className="Box-root Typography-root Typography-bodyCopy Typography-colorTextPrimary"
>
Forgot your password?
</p>
</div>
</fieldset>
<fieldset
className="FieldSet-root Box-root HorizontalGutter-root FormField-root HorizontalGutter-half"
>
<label
className="Box-root Typography-root Typography-inputLabel Typography-colorTextPrimary InputLabel-root"
htmlFor="newPassword"
>
New Password
</label>
<div
className="PasswordField-fullWidth PasswordField-root"
>
<div
className="PasswordField-wrapper"
>
<input
autoCapitalize="off"
autoComplete="new-password"
autoCorrect="off"
className="PasswordField-colorRegular PasswordField-fullWidth PasswordField-input"
disabled={false}
id="newPassword"
name="newPassword"
onBlur={[Function]}
onChange={[Function]}
onFocus={[Function]}
placeholder=""
spellCheck={false}
type="password"
value=""
/>
<div
className="PasswordField-icon"
onClick={[Function]}
role="button"
tabIndex={0}
title="Hide password"
>
<span
aria-hidden="true"
className="Icon-root Icon-sm"
>
visibility
</span>
</div>
</div>
</div>
</fieldset>
<div
className="Box-root Flex-root Flex-flex Flex-justifyFlexEnd"
>
<button
className="BaseButton-root Button-root Button-sizeRegular Button-colorPrimary Button-variantFilled Button-disabled"
disabled={true}
onBlur={[Function]}
onFocus={[Function]}
onMouseOut={[Function]}
onMouseOver={[Function]}
onTouchEnd={[Function]}
type="submit"
>
Change Password
</button>
</div>
</fieldset>
</div>
</form>
</div>
</div>
</section>
</div>
</section>
</div>
`;
@@ -1,92 +0,0 @@
import { pureMerge } from "coral-common/utils";
import { GQLResolver } from "coral-framework/schema";
import {
createResolversStub,
CreateTestRendererParams,
waitForElement,
waitUntilThrow,
within,
} from "coral-framework/testHelpers";
import { commenters, settings, stories, viewerPassive } from "../fixtures";
import create from "./create";
const story = stories[0];
const viewer = viewerPassive;
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
story: () => story,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("SETTINGS", "profileTab");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const section = await waitForElement(() =>
within(testRenderer.root).getByTestID("profile-settings-ignoredCommenters")
);
return {
testRenderer,
context,
section,
};
}
it("render empty ignored users list", async () => {
const { section } = await createTestRenderer();
await waitForElement(() =>
within(section).getByText("You are not currently ignoring anyone", {
exact: false,
})
);
});
it("render ignored users list", async () => {
const { section } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () =>
pureMerge<typeof viewer>(viewer, {
ignoredUsers: [commenters[0], commenters[1]],
}),
},
Mutation: {
removeUserIgnore: ({ variables }) => {
expectAndFail(variables).toMatchObject({
userID: commenters[0].id,
});
return {};
},
},
}),
});
within(section).getByText(commenters[0].username!);
within(section).getByText(commenters[1].username!);
// Stop ignoring first users.
within(section)
.getAllByText("Stop ignoring", { selector: "button" })[0]
.props.onClick();
// First user should dissappear from list.
await waitUntilThrow(() =>
within(section).getByText(commenters[0].username!)
);
within(section).getByText(commenters[1].username!);
});
@@ -0,0 +1,186 @@
import sinon from "sinon";
import { pureMerge } from "coral-common/utils";
import { GQLResolver } from "coral-framework/schema";
import {
act,
createResolversStub,
CreateTestRendererParams,
waitForElement,
waitUntilThrow,
within,
} from "coral-framework/testHelpers";
import {
commenters,
settings,
settingsWithoutLocalAuth,
stories,
viewerPassive,
} from "../fixtures";
import create from "./create";
const story = stories[0];
const viewer = viewerPassive;
async function createTestRenderer(
params: CreateTestRendererParams<GQLResolver> = {}
) {
const { testRenderer, context } = create({
...params,
resolvers: pureMerge(
createResolversStub<GQLResolver>({
Query: {
settings: () => settings,
viewer: () => viewer,
story: () => story,
},
}),
params.resolvers
),
initLocalState: (localRecord, source, environment) => {
localRecord.setValue("SETTINGS", "profileTab");
if (params.initLocalState) {
params.initLocalState(localRecord, source, environment);
}
},
});
const ignoredCommenters = await waitForElement(() =>
within(testRenderer.root).queryByTestID(
"profile-settings-ignoredCommenters"
)
);
const changePassword = within(testRenderer.root).queryByTestID(
"profile-settings-changePassword"
);
return {
testRenderer,
context,
ignoredCommenters,
changePassword,
};
}
it("renders the empty settings pane", async () => {
const {
testRenderer: { root },
} = await createTestRenderer();
expect(within(root).toJSON()).toMatchSnapshot();
});
it("doesn't show the change password pane when local auth is disabled", async () => {
const { changePassword } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
settings: () => settingsWithoutLocalAuth,
},
}),
});
expect(changePassword).toBeNull();
});
it("render password change form", async () => {
const updatePassword = sinon.stub().callsFake((_: any, { input }) => {
expectAndFail(input).toMatchObject({
oldPassword: "testtest",
newPassword: "testtest",
});
return {
clientMutationId: input.clientMutationId,
};
});
const { testRenderer } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Mutation: {
updatePassword,
},
}),
});
const changePassword = await waitForElement(() =>
within(testRenderer.root).getByTestID("profile-settings-changePassword")
);
const form = within(changePassword).getByType("form");
const oldPassword = await waitForElement(() =>
within(form).getByID("oldPassword", { exact: false })
);
const newPassword = await waitForElement(() =>
within(form).getByID("newPassword", { exact: false })
);
// Submit an empty form.
act(() => {
form.props.onSubmit();
});
within(changePassword).getAllByText("field is required", {
exact: false,
});
// Password too short.
act(() => {
oldPassword.props.onChange("test");
newPassword.props.onChange("test");
});
within(changePassword).getAllByText(
"Password must contain at least 8 characters",
{
exact: false,
}
);
await act(async () => {
oldPassword.props.onChange("testtest");
newPassword.props.onChange("testtest");
await form.props.onSubmit();
});
expect(updatePassword.calledOnce).toBeTruthy();
});
it("render empty ignored users list", async () => {
const { ignoredCommenters } = await createTestRenderer();
await waitForElement(() =>
within(ignoredCommenters).getByText(
"You are not currently ignoring anyone",
{
exact: false,
}
)
);
});
it("render ignored users list", async () => {
const { ignoredCommenters } = await createTestRenderer({
resolvers: createResolversStub<GQLResolver>({
Query: {
viewer: () =>
pureMerge<typeof viewer>(viewer, {
ignoredUsers: [commenters[0], commenters[1]],
}),
},
Mutation: {
removeUserIgnore: ({ variables }) => {
expectAndFail(variables).toMatchObject({
userID: commenters[0].id,
});
return {};
},
},
}),
});
within(ignoredCommenters).getByText(commenters[0].username!);
within(ignoredCommenters).getByText(commenters[1].username!);
// Stop ignoring first users.
within(ignoredCommenters)
.getAllByText("Stop ignoring", { selector: "button" })[0]
.props.onClick();
// First user should dissappear from list.
await waitUntilThrow(() =>
within(ignoredCommenters).getByText(commenters[0].username!)
);
within(ignoredCommenters).getByText(commenters[1].username!);
});