Skip to content

Commit 85431f0

Browse files
committed
refactor: nullable images/bio now spec compliant, e2e tested
1 parent 04db069 commit 85431f0

File tree

14 files changed

+196
-16
lines changed

14 files changed

+196
-16
lines changed

e2e/null-fields.spec.ts

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { test, expect } from '@playwright/test';
2+
import { register, login, generateUniqueUser } from './helpers/auth';
3+
import { registerUserViaAPI, updateUserViaAPI, createArticleViaAPI } from './helpers/api';
4+
import { createArticle, generateUniqueArticle } from './helpers/articles';
5+
import { addComment } from './helpers/comments';
6+
7+
/**
8+
* Tests for null/empty image and bio field handling.
9+
* Verifies that a default avatar SVG is shown when image is null or empty,
10+
* and that bio fields never render the literal text "null".
11+
*/
12+
13+
test.describe('Null/Empty Image and Bio Handling', () => {
14+
// Brief cooldown between tests to avoid backend rate limiting
15+
test.afterEach(async ({ context }) => {
16+
await context.close();
17+
await new Promise(resolve => setTimeout(resolve, 100));
18+
});
19+
20+
test('newly registered user should show default avatar on profile page', async ({ page }) => {
21+
const user = generateUniqueUser();
22+
await register(page, user.username, user.email, user.password);
23+
await page.goto(`/profile/${user.username}`, { waitUntil: 'load' });
24+
await page.waitForSelector('.user-img');
25+
const profileImg = page.locator('.user-img');
26+
await expect(profileImg).toBeVisible();
27+
const src = await profileImg.getAttribute('src');
28+
expect(src).toContain('default-avatar.svg');
29+
});
30+
31+
test('newly registered user should show default avatar in navbar', async ({ page }) => {
32+
const user = generateUniqueUser();
33+
await register(page, user.username, user.email, user.password);
34+
const navImg = page.locator('nav .user-pic');
35+
await expect(navImg).toBeVisible();
36+
const src = await navImg.getAttribute('src');
37+
expect(src).toContain('default-avatar.svg');
38+
});
39+
40+
test('newly registered user should show default avatar on article meta', async ({ page }) => {
41+
const user = generateUniqueUser();
42+
await register(page, user.username, user.email, user.password);
43+
const article = generateUniqueArticle();
44+
await createArticle(page, article);
45+
const articleMetaImg = page.locator('.article-meta img').first();
46+
await expect(articleMetaImg).toBeVisible();
47+
const src = await articleMetaImg.getAttribute('src');
48+
expect(src).toContain('default-avatar.svg');
49+
});
50+
51+
test('newly registered user should show default avatar in comment section', async ({ page }) => {
52+
const user = generateUniqueUser();
53+
await register(page, user.username, user.email, user.password);
54+
const article = generateUniqueArticle();
55+
await createArticle(page, article);
56+
await addComment(page, 'Test comment for avatar check');
57+
// Comment form author image
58+
const commentFormImg = page.locator('.comment-form .comment-author-img');
59+
await expect(commentFormImg).toBeVisible();
60+
const formSrc = await commentFormImg.getAttribute('src');
61+
expect(formSrc).toContain('default-avatar.svg');
62+
// Posted comment author image
63+
const commentImg = page.locator('.card:not(.comment-form) .comment-author-img').first();
64+
await expect(commentImg).toBeVisible();
65+
const commentSrc = await commentImg.getAttribute('src');
66+
expect(commentSrc).toContain('default-avatar.svg');
67+
});
68+
69+
test('setting image should display custom avatar on profile page', async ({ page, request }) => {
70+
const user = generateUniqueUser();
71+
const token = await registerUserViaAPI(request, user);
72+
const testImage = 'https://api.realworld.io/images/smiley-cyrus.jpeg';
73+
await updateUserViaAPI(request, token, { image: testImage });
74+
await login(page, user.email, user.password);
75+
await page.goto(`/profile/${user.username}`, { waitUntil: 'load' });
76+
await page.waitForSelector('.user-img');
77+
const profileImg = page.locator('.user-img');
78+
await expect(profileImg).toHaveAttribute('src', testImage);
79+
});
80+
81+
test('clearing image to empty string should restore default avatar', async ({ page, request }) => {
82+
const user = generateUniqueUser();
83+
const token = await registerUserViaAPI(request, user);
84+
// Set then clear
85+
await updateUserViaAPI(request, token, { image: 'https://api.realworld.io/images/smiley-cyrus.jpeg' });
86+
await updateUserViaAPI(request, token, { image: '' });
87+
await login(page, user.email, user.password);
88+
await page.goto(`/profile/${user.username}`, { waitUntil: 'load' });
89+
await page.waitForSelector('.user-img');
90+
const profileImg = page.locator('.user-img');
91+
const src = await profileImg.getAttribute('src');
92+
expect(src).toContain('default-avatar.svg');
93+
});
94+
95+
test('null bio should not render as literal "null" on profile page', async ({ page }) => {
96+
const user = generateUniqueUser();
97+
await register(page, user.username, user.email, user.password);
98+
await page.goto(`/profile/${user.username}`, { waitUntil: 'load' });
99+
await page.waitForSelector('.user-info');
100+
const bioText = await page.locator('.user-info p').textContent();
101+
expect(bioText?.trim()).not.toBe('null');
102+
expect(bioText?.trim()).toBe('');
103+
});
104+
105+
test('setting then clearing bio should not show stale data', async ({ page, request }) => {
106+
const user = generateUniqueUser();
107+
const token = await registerUserViaAPI(request, user);
108+
const testBio = 'This is a test bio';
109+
await updateUserViaAPI(request, token, { bio: testBio });
110+
await updateUserViaAPI(request, token, { bio: '' });
111+
await login(page, user.email, user.password);
112+
await page.goto(`/profile/${user.username}`, { waitUntil: 'load' });
113+
await page.waitForSelector('.user-info');
114+
const bioText = await page.locator('.user-info p').textContent();
115+
expect(bioText?.trim()).not.toBe(testBio);
116+
expect(bioText?.trim()).not.toBe('null');
117+
});
118+
119+
test('settings form should show empty string for null image', async ({ page }) => {
120+
const user = generateUniqueUser();
121+
await register(page, user.username, user.email, user.password);
122+
await page.goto('/settings', { waitUntil: 'load' });
123+
await expect(page.locator('input[formControlName="image"]')).toHaveValue('');
124+
});
125+
126+
test('settings form should show empty string for null bio', async ({ page }) => {
127+
const user = generateUniqueUser();
128+
await register(page, user.username, user.email, user.password);
129+
await page.goto('/settings', { waitUntil: 'load' });
130+
await expect(page.locator('textarea[formControlName="bio"]')).toHaveValue('');
131+
});
132+
133+
test('default avatar should display on other user articles in feed', async ({ page, request }) => {
134+
// Create a user with no image who has an article
135+
const author = generateUniqueUser();
136+
const token = await registerUserViaAPI(request, author);
137+
const uniqueId = Date.now();
138+
await createArticleViaAPI(request, token, {
139+
title: `Null avatar test ${uniqueId}`,
140+
description: `Description ${uniqueId}`,
141+
body: `Body content ${uniqueId}`,
142+
});
143+
// View the article as a different user and check the author avatar
144+
const viewer = generateUniqueUser();
145+
await register(page, viewer.username, viewer.email, viewer.password);
146+
await page.goto('/', { waitUntil: 'load' });
147+
// Find the article in the global feed
148+
await page.locator('a.nav-link', { hasText: 'Global Feed' }).click();
149+
await page.waitForSelector('.article-preview', { timeout: 10000 });
150+
const articlePreview = page.locator('.article-preview', { hasText: `Null avatar test ${uniqueId}` });
151+
await expect(articlePreview).toBeVisible();
152+
// The author avatar in the article preview should be the default
153+
const authorImg = articlePreview.locator('.article-meta img');
154+
const src = await authorImg.getAttribute('src');
155+
expect(src).toContain('default-avatar.svg');
156+
});
157+
});

src/app/core/auth/user.model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,6 @@ export interface User {
22
email: string;
33
token: string;
44
username: string;
5-
bio: string;
6-
image: string;
5+
bio: string | null;
6+
image: string | null;
77
}

src/app/core/layout/header.component.html

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@
4444
@if (currentUser$ | async; as currentUser) {
4545
<li class="nav-item">
4646
<a class="nav-link" [routerLink]="['/profile', currentUser.username]" routerLinkActive="active">
47-
@if (currentUser.image) {
48-
<img [src]="currentUser.image" class="user-pic" />
49-
}
47+
<img [src]="currentUser.image | defaultImage" class="user-pic" />
5048
{{ currentUser.username }}
5149
</a>
5250
</li>

src/app/core/layout/header.component.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
22
import { UserService } from '../auth/services/user.service';
33
import { RouterLink, RouterLinkActive } from '@angular/router';
44
import { AsyncPipe } from '@angular/common';
5+
import { DefaultImagePipe } from '../../shared/pipes/default-image.pipe';
56

67
@Component({
78
selector: 'app-layout-header',
89
templateUrl: './header.component.html',
9-
imports: [RouterLinkActive, RouterLink, AsyncPipe],
10+
imports: [RouterLinkActive, RouterLink, AsyncPipe, DefaultImagePipe],
1011
changeDetection: ChangeDetectionStrategy.OnPush,
1112
})
1213
export class HeaderComponent {

src/app/features/article/components/article-comment.component.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { RouterLink } from '@angular/router';
55
import { map } from 'rxjs/operators';
66
import { Comment } from '../models/comment.model';
77
import { AsyncPipe, DatePipe } from '@angular/common';
8+
import { DefaultImagePipe } from '../../../shared/pipes/default-image.pipe';
89

910
@Component({
1011
selector: 'app-article-comment',
@@ -18,7 +19,7 @@ import { AsyncPipe, DatePipe } from '@angular/common';
1819
</div>
1920
<div class="card-footer">
2021
<a class="comment-author" [routerLink]="['/profile', comment.author.username]">
21-
<img [src]="comment.author.image" class="comment-author-img" />
22+
<img [src]="comment.author.image | defaultImage" class="comment-author-img" />
2223
</a>
2324
&nbsp;
2425
<a class="comment-author" [routerLink]="['/profile', comment.author.username]">
@@ -36,7 +37,7 @@ import { AsyncPipe, DatePipe } from '@angular/common';
3637
</div>
3738
}
3839
`,
39-
imports: [RouterLink, DatePipe, AsyncPipe],
40+
imports: [RouterLink, DatePipe, AsyncPipe, DefaultImagePipe],
4041
changeDetection: ChangeDetectionStrategy.OnPush,
4142
})
4243
export class ArticleCommentComponent {

src/app/features/article/components/article-meta.component.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core';
22
import { Article } from '../models/article.model';
33
import { RouterLink } from '@angular/router';
44
import { DatePipe } from '@angular/common';
5+
import { DefaultImagePipe } from '../../../shared/pipes/default-image.pipe';
56

67
@Component({
78
selector: 'app-article-meta',
89
template: `
910
<div class="article-meta">
1011
<a [routerLink]="['/profile', article.author.username]">
11-
<img [src]="article.author.image" />
12+
<img [src]="article.author.image | defaultImage" />
1213
</a>
1314
1415
<div class="info">
@@ -24,7 +25,7 @@ import { DatePipe } from '@angular/common';
2425
</div>
2526
`,
2627
changeDetection: ChangeDetectionStrategy.OnPush,
27-
imports: [RouterLink, DatePipe],
28+
imports: [RouterLink, DatePipe, DefaultImagePipe],
2829
})
2930
export class ArticleMetaComponent {
3031
@Input() article!: Article;

src/app/features/article/pages/article/article.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ <h1>{{ a.title }}</h1>
103103
></textarea>
104104
</div>
105105
<div class="card-footer">
106-
<img [src]="currentUser()?.image === null ? '' : currentUser()?.image" class="comment-author-img" />
106+
<img [src]="currentUser()?.image | defaultImage" class="comment-author-img" />
107107
<button class="btn btn-sm btn-primary" type="submit">Post Comment</button>
108108
</div>
109109
</fieldset>

src/app/features/article/pages/article/article.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { Profile } from '../../../profile/models/profile.model';
2020
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
2121
import { FavoriteButtonComponent } from '../../components/favorite-button.component';
2222
import { FollowButtonComponent } from '../../../profile/components/follow-button.component';
23+
import { DefaultImagePipe } from '../../../../shared/pipes/default-image.pipe';
2324

2425
@Component({
2526
selector: 'app-article-page',
@@ -37,6 +38,7 @@ import { FollowButtonComponent } from '../../../profile/components/follow-button
3738
ArticleCommentComponent,
3839
ReactiveFormsModule,
3940
IfAuthenticatedDirective,
41+
DefaultImagePipe,
4042
],
4143
changeDetection: ChangeDetectionStrategy.OnPush,
4244
})
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
export interface Profile {
22
username: string;
3-
bio: string;
4-
image: string;
3+
bio: string | null;
4+
image: string | null;
55
following: boolean;
66
}

src/app/features/profile/pages/profile/profile.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,9 @@
1313
<div class="container">
1414
<div class="row">
1515
<div class="col-xs-12 col-md-10 offset-md-1">
16-
<img [src]="p.image" class="user-img" />
16+
<img [src]="p.image | defaultImage" class="user-img" />
1717
<h4>{{ p.username }}</h4>
18-
<p>{{ p.bio }}</p>
18+
<p>{{ p.bio ?? '' }}</p>
1919
@if (!isUser()) {
2020
<app-follow-button [profile]="p" (toggle)="onToggleFollowing($event)" />
2121
}

0 commit comments

Comments
 (0)