Skip to content

Commit 2bdf147

Browse files
committed
feat(components): added an example password input component
1 parent bcac669 commit 2bdf147

File tree

9 files changed

+320
-14
lines changed

9 files changed

+320
-14
lines changed

package-lock.json

Lines changed: 2 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/components/Dropdown/Dropdown.spec.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
1-
import { test as componentTest } from '@sand4rt/experimental-ct-web';
21
import { Ensure, equals } from '@serenity-js/assertions';
32
import { notes, TakeNotes } from '@serenity-js/core';
4-
import { useBase } from '@serenity-js/playwright-test';
53
import { PageElement } from '@serenity-js/web';
64

7-
import { DropdownOption, default as DropdownComponent } from './Dropdown.js';
5+
import { default as DropdownComponent, DropdownOption } from './Dropdown.js';
86
import { Dropdown } from './Dropdown.serenity.js';
97

10-
const { it, describe } = useBase(componentTest);
8+
import { describe, it } from '../test-api';
119

1210
describe('Dropdown', () => {
1311

@@ -136,7 +134,10 @@ describe('Dropdown', () => {
136134
);
137135
});
138136

139-
it('goes back to showing the placeholder when all the selected options get deselected', async ({ mount, actor }) => {
137+
it('goes back to showing the placeholder when all the selected options get deselected', async ({
138+
mount,
139+
actor
140+
}) => {
140141
const placeholder = 'Select option';
141142

142143
const dropdownComponent = PageElement.from(await mount(DropdownComponent, {
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { Answerable, d, QuestionAdapter, Task } from '@serenity-js/core';
2+
import { By, Click, Enter, PageElement, Value } from '@serenity-js/web';
3+
4+
export class Password {
5+
private static componentSelector = () => By.deepCss('.password-widget')
6+
7+
static component = <NET = any>() =>
8+
PageElement.located<NET>(this.componentSelector()).describedAs('password field')
9+
10+
static components = <NET = any>() =>
11+
PageElement.located<NET>(this.componentSelector()).describedAs('password fields')
12+
13+
private static inputField = () =>
14+
PageElement.located(By.deepCss('.password-input'))
15+
.describedAs('input field')
16+
17+
private static toggleButton = () =>
18+
PageElement.located(By.deepCss('.password-toggle-button'))
19+
.describedAs('toggle visibility button')
20+
21+
static value = () => ({
22+
of: (passwordField: QuestionAdapter<PageElement>) =>
23+
Value.of(Password.inputField())
24+
.of(passwordField)
25+
.describedAs('password value')
26+
})
27+
28+
static type = (password: Answerable<string>) => ({
29+
into: (passwordField: QuestionAdapter<PageElement>) =>
30+
Task.where(d`#actor types password into ${passwordField}`,
31+
Enter.theValue(password).into(Password.inputField().of(passwordField)),
32+
)
33+
})
34+
35+
static reveal = (passwordField: QuestionAdapter<PageElement>) =>
36+
Task.where(`#actor reveals the password in ${passwordField}`,
37+
Click.on(Password.toggleButton().of(passwordField)),
38+
)
39+
40+
static hide = (passwordField: QuestionAdapter<PageElement>) =>
41+
Task.where(`#actor hides the password in ${passwordField}`,
42+
Click.on(Password.toggleButton().of(passwordField)),
43+
)
44+
45+
static toggle = (passwordField: QuestionAdapter<PageElement>) =>
46+
Task.where(`#actor toggles password visibility in ${passwordField}`,
47+
Click.on(Password.toggleButton().of(passwordField)),
48+
)
49+
50+
static isVisible = () => ({
51+
in: (passwordField: QuestionAdapter<PageElement>) =>
52+
Password.inputField()
53+
.of(passwordField)
54+
.attribute('type')
55+
.describedAs('password visibility state')
56+
.as(type => type === 'text')
57+
})
58+
}
59+
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { Ensure, equals, isFalse, isTrue } from '@serenity-js/assertions';
2+
import { notes, TakeNotes } from '@serenity-js/core';
3+
import { Attribute, By, PageElement } from '@serenity-js/web';
4+
5+
import { default as PasswordComponent } from './Password.js';
6+
import { Password } from './Password.serenity.js';
7+
8+
import { describe, it } from '../test-api';
9+
10+
describe('Password', () => {
11+
12+
it('hides the password by default', async ({ mount, actor }) => {
13+
const passwordField = PageElement.from(await mount(PasswordComponent, {
14+
props: {
15+
placeholder: 'Enter password',
16+
},
17+
})).describedAs('password field')
18+
19+
await actor.attemptsTo(
20+
Ensure.that(
21+
Password.isVisible().in(passwordField),
22+
isFalse()
23+
),
24+
)
25+
});
26+
27+
it('reveals the password when the eye icon is clicked', async ({ mount, actor }) => {
28+
const passwordField = PageElement.from(await mount(PasswordComponent, {
29+
props: {
30+
placeholder: 'Enter password',
31+
},
32+
})).describedAs('password field')
33+
34+
await actor.attemptsTo(
35+
Ensure.that(Password.isVisible().in(passwordField), isFalse()),
36+
37+
Password.reveal(passwordField),
38+
39+
Ensure.that(Password.isVisible().in(passwordField), isTrue()),
40+
)
41+
});
42+
43+
it('hides the password when the eye icon is clicked again', async ({ mount, actor }) => {
44+
const passwordField = PageElement.from(await mount(PasswordComponent, {
45+
props: {
46+
placeholder: 'Enter password',
47+
},
48+
})).describedAs('password field')
49+
50+
await actor.attemptsTo(
51+
Password.reveal(passwordField),
52+
Ensure.that(Password.isVisible().in(passwordField), isTrue()),
53+
54+
Password.hide(passwordField),
55+
56+
Ensure.that(Password.isVisible().in(passwordField), isFalse()),
57+
)
58+
});
59+
60+
it('allows typing a password', async ({ mount, actor }) => {
61+
const testPassword = 'SecurePassword123!';
62+
63+
const passwordField = PageElement.from(await mount(PasswordComponent, {
64+
props: {
65+
placeholder: 'Enter password',
66+
},
67+
})).describedAs('password field')
68+
69+
await actor.attemptsTo(
70+
Password.type(testPassword).into(passwordField),
71+
72+
Ensure.that(
73+
Password.value().of(passwordField),
74+
equals(testPassword)
75+
),
76+
)
77+
});
78+
79+
80+
it('triggers visibilitychange event when toggling', async ({ mount, actor }) => {
81+
function spy<Arguments extends any[]>(name: string): (spyArguments: Arguments) => void {
82+
return (spyArguments: Arguments) => TakeNotes.as(actor).notepad.set(name, spyArguments);
83+
}
84+
85+
const passwordField = PageElement.from(await mount(PasswordComponent, {
86+
props: {
87+
placeholder: 'Enter password',
88+
},
89+
on: {
90+
visibilitychange: spy('visibilityState'),
91+
}
92+
})).describedAs('password field')
93+
94+
await actor.attemptsTo(
95+
Password.toggle(passwordField),
96+
97+
Ensure.eventually(notes().get('visibilityState'), isTrue()),
98+
99+
Password.toggle(passwordField),
100+
101+
Ensure.eventually(notes().get('visibilityState'), isFalse()),
102+
)
103+
});
104+
105+
it('displays the placeholder text', async ({ mount, actor }) => {
106+
const placeholder = 'Enter your secure password';
107+
108+
const passwordField = PageElement.from(await mount(PasswordComponent, {
109+
props: {
110+
placeholder,
111+
},
112+
})).describedAs('password field')
113+
114+
await actor.attemptsTo(
115+
Ensure.that(
116+
Attribute.called('placeholder')
117+
.of(PageElement.located(By.deepCss('.password-input')).of(passwordField)),
118+
equals(placeholder)
119+
),
120+
)
121+
});
122+
});
123+

0 commit comments

Comments
 (0)