mirror of
https://github.com/wassname/talk.git
synced 2026-09-10 12:43:11 +08:00
[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:
@@ -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!);
|
||||
});
|
||||
Reference in New Issue
Block a user