Skip to content

Commit 81a1a16

Browse files
committed
feat(): add new signal form challenge
1 parent b263d0f commit 81a1a16

27 files changed

Lines changed: 1206 additions & 78 deletions

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,5 @@ TODO.md
4949

5050
package-lock.json
5151
npm-shrinkwrap.json
52+
53+
__screenshots__/

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ If you would like to propose a challenge, this project is open source, so feel f
2929
3030
## Challenges
3131

32-
Check [all 60 challenges](https://angular-challenges.vercel.app/)
32+
Check [all 61 challenges](https://angular-challenges.vercel.app/)
3333

3434
## Contributors ✨
3535

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"extends": ["../../../.eslintrc.json"],
3+
"ignorePatterns": ["!**/*"],
4+
"overrides": [
5+
{
6+
"files": ["*.ts"],
7+
"extends": [
8+
"plugin:@nx/angular",
9+
"plugin:@angular-eslint/template/process-inline-templates"
10+
],
11+
"rules": {
12+
"@angular-eslint/directive-selector": [
13+
"error",
14+
{
15+
"type": "attribute",
16+
"prefix": "app",
17+
"style": "camelCase"
18+
}
19+
],
20+
"@angular-eslint/component-selector": [
21+
"error",
22+
{
23+
"type": "element",
24+
"prefix": "app",
25+
"style": "kebab-case"
26+
}
27+
]
28+
}
29+
},
30+
{
31+
"files": ["*.html"],
32+
"extends": ["plugin:@nx/angular-template"],
33+
"rules": {}
34+
}
35+
]
36+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# simplest signal form
2+
3+
> author: thomas-laforge
4+
5+
### Run Application
6+
7+
```bash
8+
npx nx serve forms-simplest-signal-form
9+
```
10+
11+
### Documentation and Instruction
12+
13+
Challenge documentation is [here](https://angular-challenges.vercel.app/challenges/forms/61-simplest-signal-form/).
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
{
2+
"name": "forms-simplest-signal-form",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"projectType": "application",
5+
"prefix": "app",
6+
"sourceRoot": "apps/forms/61-simplest-signal-form/src",
7+
"tags": [],
8+
"targets": {
9+
"build": {
10+
"executor": "@angular/build:application",
11+
"outputs": ["{options.outputPath}"],
12+
"options": {
13+
"outputPath": "dist/apps/forms/61-simplest-signal-form",
14+
"browser": "apps/forms/61-simplest-signal-form/src/main.ts",
15+
"tsConfig": "apps/forms/61-simplest-signal-form/tsconfig.app.json",
16+
"inlineStyleLanguage": "scss",
17+
"assets": [
18+
{
19+
"glob": "**/*",
20+
"input": "apps/forms/61-simplest-signal-form/public"
21+
}
22+
],
23+
"styles": ["apps/forms/61-simplest-signal-form/src/styles.scss"]
24+
},
25+
"configurations": {
26+
"production": {
27+
"budgets": [
28+
{
29+
"type": "initial",
30+
"maximumWarning": "500kb",
31+
"maximumError": "1mb"
32+
},
33+
{
34+
"type": "anyComponentStyle",
35+
"maximumWarning": "4kb",
36+
"maximumError": "8kb"
37+
}
38+
],
39+
"outputHashing": "all"
40+
},
41+
"development": {
42+
"optimization": false,
43+
"extractLicenses": false,
44+
"sourceMap": true
45+
}
46+
},
47+
"defaultConfiguration": "production"
48+
},
49+
"serve": {
50+
"continuous": true,
51+
"executor": "@angular/build:dev-server",
52+
"configurations": {
53+
"production": {
54+
"buildTarget": "forms-simplest-signal-form:build:production"
55+
},
56+
"development": {
57+
"buildTarget": "forms-simplest-signal-form:build:development"
58+
}
59+
},
60+
"defaultConfiguration": "development"
61+
},
62+
"lint": {
63+
"executor": "@nx/eslint:lint"
64+
},
65+
"test": {
66+
"executor": "@angular/build:unit-test",
67+
"options": {}
68+
},
69+
"serve-static": {
70+
"continuous": true,
71+
"executor": "@nx/web:file-server",
72+
"options": {
73+
"buildTarget": "forms-simplest-signal-form:build",
74+
"staticFilePath": "dist/apps/forms/61-simplest-signal-form/browser",
75+
"spa": true
76+
}
77+
}
78+
}
79+
}
14.7 KB
Binary file not shown.
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
import { render, screen } from '@testing-library/angular';
2+
import '@testing-library/jest-dom';
3+
import userEvent from '@testing-library/user-event';
4+
import { AppComponent } from './app.component';
5+
6+
describe('AppComponent', () => {
7+
describe('When component is rendered', () => {
8+
it('Then should display the form title', async () => {
9+
await render(AppComponent);
10+
11+
expect(screen.getByText('Simple Form')).toBeInTheDocument();
12+
});
13+
14+
it('Then should display all form fields', async () => {
15+
await render(AppComponent);
16+
17+
expect(screen.getByLabelText(/^Name/i)).toBeInTheDocument();
18+
expect(screen.getByLabelText(/last name/i)).toBeInTheDocument();
19+
expect(screen.getByLabelText(/age/i)).toBeInTheDocument();
20+
expect(screen.getByLabelText(/note/i)).toBeInTheDocument();
21+
});
22+
23+
it('Then submit button should be disabled initially', async () => {
24+
await render(AppComponent);
25+
26+
const submitButton = screen.getByRole('button', { name: /submit/i });
27+
expect(submitButton).toBeDisabled();
28+
});
29+
});
30+
31+
describe('Given valid form data', () => {
32+
describe('When user fills in all required fields', () => {
33+
it('Then submit button should be enabled', async () => {
34+
const user = userEvent.setup();
35+
await render(AppComponent);
36+
37+
const nameInput = screen.getByLabelText(/^Name/i);
38+
await user.type(nameInput, 'John');
39+
40+
const submitButton = screen.getByRole('button', { name: /submit/i });
41+
expect(submitButton).toBeEnabled();
42+
});
43+
});
44+
45+
describe('When user submits the form', () => {
46+
it('Then should display submitted data', async () => {
47+
const user = userEvent.setup();
48+
await render(AppComponent);
49+
50+
const nameInput = screen.getByLabelText(/^Name/i);
51+
const lastnameInput = screen.getByLabelText(/last name/i);
52+
const ageInput = screen.getByLabelText(/age/i);
53+
const noteInput = screen.getByLabelText(/note/i);
54+
55+
await user.type(nameInput, 'John');
56+
await user.type(lastnameInput, 'Doe');
57+
await user.type(ageInput, '25');
58+
await user.type(noteInput, 'Test note');
59+
60+
const submitButton = screen.getByRole('button', { name: /submit/i });
61+
await user.click(submitButton);
62+
63+
expect(screen.getByText('Submitted Data:')).toBeInTheDocument();
64+
expect(screen.getByText(/"name": "John"/)).toBeInTheDocument();
65+
expect(screen.getByText(/"lastname": "Doe"/)).toBeInTheDocument();
66+
expect(screen.getByText(/"age": 25/)).toBeInTheDocument();
67+
expect(screen.getByText(/"note": "Test note"/)).toBeInTheDocument();
68+
});
69+
});
70+
});
71+
72+
describe('Given name field validation', () => {
73+
describe('When name field is empty and touched', () => {
74+
it('Then should display required error', async () => {
75+
const user = userEvent.setup();
76+
await render(AppComponent);
77+
78+
const nameInput = screen.getByLabelText(/^Name/i);
79+
await user.click(nameInput);
80+
await user.tab();
81+
82+
expect(screen.getByText('Name is required')).toBeInTheDocument();
83+
});
84+
85+
it('Then submit button should be disabled', async () => {
86+
const user = userEvent.setup();
87+
await render(AppComponent);
88+
89+
const nameInput = screen.getByLabelText(/^Name/i);
90+
await user.click(nameInput);
91+
await user.tab();
92+
93+
const submitButton = screen.getByRole('button', { name: /submit/i });
94+
expect(submitButton).toBeDisabled();
95+
});
96+
});
97+
});
98+
99+
describe('Given age field validation', () => {
100+
describe('When age is less than 1', () => {
101+
it('Then should display min error', async () => {
102+
const user = userEvent.setup();
103+
await render(AppComponent);
104+
105+
const nameInput = screen.getByLabelText(/^Name/i);
106+
const ageInput = screen.getByLabelText(/age/i);
107+
108+
await user.type(nameInput, 'John');
109+
await user.type(ageInput, '0');
110+
await user.tab();
111+
112+
expect(screen.getByText('Age must be at least 1')).toBeInTheDocument();
113+
});
114+
});
115+
116+
describe('When age is greater than 99', () => {
117+
it('Then should display max error', async () => {
118+
const user = userEvent.setup();
119+
await render(AppComponent);
120+
121+
const nameInput = screen.getByLabelText(/^Name/i);
122+
const ageInput = screen.getByLabelText(/age/i);
123+
124+
await user.type(nameInput, 'John');
125+
await user.type(ageInput, '100');
126+
await user.tab();
127+
128+
expect(screen.getByText('Age must be at most 99')).toBeInTheDocument();
129+
});
130+
});
131+
132+
describe('When age is between 1 and 99', () => {
133+
it('Then should not display any error', async () => {
134+
const user = userEvent.setup();
135+
await render(AppComponent);
136+
137+
const nameInput = screen.getByLabelText(/^Name/i);
138+
const ageInput = screen.getByLabelText(/age/i);
139+
140+
await user.type(nameInput, 'John');
141+
await user.type(ageInput, '50');
142+
await user.tab();
143+
144+
expect(screen.queryByText(/age must be/i)).not.toBeInTheDocument();
145+
});
146+
});
147+
});
148+
149+
describe('Given optional fields', () => {
150+
describe('When lastname and note are empty', () => {
151+
it('Then should still allow form submission with valid name', async () => {
152+
const user = userEvent.setup();
153+
await render(AppComponent);
154+
155+
const nameInput = screen.getByLabelText(/^Name/i);
156+
await user.type(nameInput, 'John');
157+
158+
const submitButton = screen.getByRole('button', { name: /submit/i });
159+
expect(submitButton).toBeEnabled();
160+
161+
await user.click(submitButton);
162+
163+
expect(screen.getByText('Submitted Data:')).toBeInTheDocument();
164+
});
165+
});
166+
});
167+
168+
describe('Given reset functionality', () => {
169+
describe('When user clicks reset button after filling form', () => {
170+
it('Then should clear all form fields', async () => {
171+
const user = userEvent.setup();
172+
await render(AppComponent);
173+
174+
const nameInput = screen.getByLabelText(/^Name/i) as HTMLInputElement;
175+
const lastnameInput = screen.getByLabelText(
176+
/last name/i,
177+
) as HTMLInputElement;
178+
const ageInput = screen.getByLabelText(/age/i) as HTMLInputElement;
179+
const noteInput = screen.getByLabelText(/note/i) as HTMLInputElement;
180+
181+
await user.type(nameInput, 'John');
182+
await user.type(lastnameInput, 'Doe');
183+
await user.type(ageInput, '25');
184+
await user.type(noteInput, 'Test note');
185+
186+
const resetButton = screen.getByRole('button', { name: /reset/i });
187+
await user.click(resetButton);
188+
189+
expect(nameInput.value).toBe('');
190+
expect(lastnameInput.value).toBe('');
191+
expect(ageInput.value).toBe('');
192+
expect(noteInput.value).toBe('');
193+
});
194+
195+
it('Then should hide submitted data if present', async () => {
196+
const user = userEvent.setup();
197+
await render(AppComponent);
198+
199+
const nameInput = screen.getByLabelText(/^Name/i);
200+
await user.type(nameInput, 'John');
201+
202+
const submitButton = screen.getByRole('button', { name: /submit/i });
203+
await user.click(submitButton);
204+
205+
expect(screen.getByText('Submitted Data:')).toBeInTheDocument();
206+
207+
const resetButton = screen.getByRole('button', { name: /reset/i });
208+
await user.click(resetButton);
209+
210+
expect(screen.queryByText('Submitted Data:')).not.toBeInTheDocument();
211+
});
212+
});
213+
});
214+
215+
describe('Given form styling', () => {
216+
describe('When field has validation error', () => {
217+
it('Then should display red border on name field', async () => {
218+
const user = userEvent.setup();
219+
await render(AppComponent);
220+
221+
const nameInput = screen.getByLabelText(/^Name/i);
222+
await user.click(nameInput);
223+
await user.tab();
224+
225+
expect(nameInput).toHaveClass('border-red-500');
226+
});
227+
228+
it('Then should display red border on age field when invalid', async () => {
229+
const user = userEvent.setup();
230+
await render(AppComponent);
231+
232+
const nameInput = screen.getByLabelText(/^Name/i);
233+
const ageInput = screen.getByLabelText(/age/i);
234+
235+
await user.type(nameInput, 'John');
236+
await user.type(ageInput, '0');
237+
await user.tab();
238+
239+
expect(ageInput).toHaveClass('border-red-500');
240+
});
241+
});
242+
});
243+
});

0 commit comments

Comments
 (0)