Skip to content

Commit

Permalink
fix tests
Browse files Browse the repository at this point in the history
  • Loading branch information
uwon0625 committed Dec 16, 2024
1 parent 6bd1efb commit 99097ad
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 139 deletions.
51 changes: 30 additions & 21 deletions NewsApi.Tests/Services/HackerNewsServiceTests.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
#nullable enable

using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
Expand Down Expand Up @@ -216,8 +218,8 @@ public async Task GetItemAsync_AddsToStoriesCache_WhenFetchingNewStory()
cacheEntry.SetupAllProperties();

_cacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object>.IsAny))
.Callback(new OutCallback((object k, out object v) =>
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object?>.IsAny))
.Callback(new OutCallback((object k, out object? v) =>
{
if (k.ToString() == _storiesCacheKey)
{
Expand All @@ -228,7 +230,7 @@ public async Task GetItemAsync_AddsToStoriesCache_WhenFetchingNewStory()
v = null;
}
}))
.Returns<object, object>((k, v) => k.ToString() == _storiesCacheKey);
.Returns<object, object?>((k, v) => k.ToString() == _storiesCacheKey);

_cacheMock
.Setup(m => m.CreateEntry(It.IsAny<object>()))
Expand Down Expand Up @@ -299,8 +301,8 @@ public async Task GetNewStoriesAsync_HandlesCacheMiss_AndPopulatesCache()

// Setup cache mocks
_cacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object>.IsAny))
.Callback(new OutCallback((object k, out object v) =>
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object?>.IsAny))
.Callback(new OutCallback((object k, out object? v) =>
{
if (k.ToString() == _storiesCacheKey)
{
Expand All @@ -315,7 +317,7 @@ public async Task GetNewStoriesAsync_HandlesCacheMiss_AndPopulatesCache()
v = null;
}
}))
.Returns<object, object>((k, v) => k.ToString() == _storiesCacheKey);
.Returns<object, object?>((k, v) => k.ToString() == _storiesCacheKey);

_cacheMock
.Setup(m => m.CreateEntry(It.IsAny<object>()))
Expand Down Expand Up @@ -368,8 +370,8 @@ public async Task GetStoriesAsync_RespectsCacheExpiration()

// Setup cache mocks
_cacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object>.IsAny))
.Callback(new OutCallback((object k, out object v) => v = null))
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object?>.IsAny))
.Callback(new OutCallback((object k, out object? v) => v = null))
.Returns(false);

_cacheMock
Expand Down Expand Up @@ -547,19 +549,26 @@ public async Task GetStoriesAsync_HandlesNonSequentialIds()
Assert.Equal(1, result[2].Id);
}

private void SetupHttpMockResponse<T>(string url, T response)
private void SetupHttpMockResponse<T>(string url, T? response)
{
string content = response switch
{
null => "null",
int intValue => intValue.ToString(),
_ => System.Text.Json.JsonSerializer.Serialize(response)
};

_handlerMock
.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri!.PathAndQuery.Contains(url)),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage
.ReturnsAsync(() => new HttpResponseMessage
{
StatusCode = HttpStatusCode.OK,
Content = new StringContent(System.Text.Json.JsonSerializer.Serialize(response))
Content = new StringContent(content)
});
}

Expand All @@ -571,8 +580,8 @@ private void SetupDefaultMocks()

// Setup cache mocks
_cacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object>.IsAny))
.Callback(new OutCallback((object k, out object v) =>
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object?>.IsAny))
.Callback(new OutCallback((object k, out object? v) =>
{
if (k.ToString() == _storiesCacheKey)
{
Expand All @@ -583,7 +592,7 @@ private void SetupDefaultMocks()
v = null;
}
}))
.Returns<object, object>((k, v) => k.ToString() == _storiesCacheKey);
.Returns<object, object?>((k, v) => k.ToString() == _storiesCacheKey);

_cacheMock
.Setup(m => m.CreateEntry(It.IsAny<object>()))
Expand Down Expand Up @@ -616,8 +625,8 @@ private void SetupCacheWithStories(Dictionary<int, NewsItem> stories)

// Setup cache mocks
_cacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object>.IsAny))
.Callback(new OutCallback((object k, out object v) =>
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object?>.IsAny))
.Callback(new OutCallback((object k, out object? v) =>
{
if (k.ToString() == _storiesCacheKey)
{
Expand All @@ -628,7 +637,7 @@ private void SetupCacheWithStories(Dictionary<int, NewsItem> stories)
v = null;
}
}))
.Returns<object, object>((k, v) => k.ToString() == _storiesCacheKey);
.Returns<object, object?>((k, v) => k.ToString() == _storiesCacheKey);

_cacheMock
.Setup(m => m.CreateEntry(It.IsAny<object>()))
Expand All @@ -645,8 +654,8 @@ private void SetupCacheWithNewStories()
cacheEntry.SetupAllProperties();

_cacheMock
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object>.IsAny))
.Callback(new OutCallback((object k, out object v) =>
.Setup(m => m.TryGetValue(It.IsAny<object>(), out It.Ref<object?>.IsAny))
.Callback(new OutCallback((object k, out object? v) =>
{
if (k.ToString() == _storiesCacheKey)
v = _storiesCache;
Expand All @@ -655,7 +664,7 @@ private void SetupCacheWithNewStories()
else
v = null;
}))
.Returns<object, object>((k, v) => k.ToString() == _storiesCacheKey);
.Returns<object, object?>((k, v) => k.ToString() == _storiesCacheKey);

_cacheMock
.Setup(m => m.CreateEntry(It.IsAny<object>()))
Expand All @@ -665,7 +674,7 @@ private void SetupCacheWithNewStories()
}

// Add this delegate at class level
private delegate void OutCallback(object key, out object value);
private delegate void OutCallback(object key, out object? value);

private void SetupAllConfiguration()
{
Expand Down
164 changes: 47 additions & 117 deletions client-app/e2e/story-list.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,156 +2,86 @@ import { test, expect } from '@playwright/test';

test.describe('Story List', () => {
test.beforeEach(async ({ page }) => {
// Add retry logic for connection with longer timeout
let retries = 3;
while (retries > 0) {
try {
await page.goto('http://localhost:4200', {
waitUntil: 'networkidle',
timeout: 90000 // 90 seconds timeout
});

// Wait for either stories to load or error state
await Promise.race([
page.waitForSelector('app-story-item', { timeout: 30000 }),
page.waitForSelector('.error', { timeout: 30000 })
]);
break;
} catch (error) {
console.log(`Retry attempt ${4 - retries}, Error:`, error);
retries--;
if (retries === 0) throw error;
await new Promise(resolve => setTimeout(resolve, 10000)); // Wait 10s before retry
}
}
await page.goto('http://localhost:4200');
// Wait for initial stories to load and be visible
await expect(page.locator('app-story-item')).toHaveCount(10, { timeout: 30000 });
});

test('should load initial stories', async ({ page }) => {
// Wait for stories to load
await page.waitForSelector('app-story-item');

// Check if we have the correct number of stories
const stories = await page.locator('app-story-item').all();
expect(stories.length).toBe(10);
});

test('should handle pagination', async ({ page }) => {
// Wait for initial load
await page.waitForSelector('app-story-item');

// Get initial first story title
const firstStoryTitle = await page.locator('app-story-item').first().textContent();
// Store first story title for comparison
const firstStoryTitle = await page.locator('app-story-item .story-title').first().textContent();

// Click next page
// Click next and wait for stories to change
await page.getByRole('button', { name: 'Next' }).click();

// Wait for new stories to load
await page.waitForTimeout(500);
// Wait for stories to change
await expect(async () => {
const newTitle = await page.locator('app-story-item .story-title').first().textContent();
expect(newTitle).not.toBe(firstStoryTitle);
}).toPass({ timeout: 30000 });

// Get new first story title
const newFirstStoryTitle = await page.locator('app-story-item').first().textContent();

// Verify it's different
expect(newFirstStoryTitle).not.toBe(firstStoryTitle);
// Verify we still have 10 stories
await expect(page.locator('app-story-item')).toHaveCount(10);
});

test('should change page size', async ({ page }) => {
// Wait for initial load
await page.waitForSelector('app-story-item');

// Change page size to 5
await page.selectOption('select#pageSizeSelect', '5');

// Wait for update
await page.waitForTimeout(500);

// Check number of stories
const stories = await page.locator('app-story-item').all();
expect(stories.length).toBe(5);
// Wait for story count to change
await expect(page.locator('app-story-item')).toHaveCount(5, { timeout: 10000 });
});

test('should search stories', async ({ page }) => {
// Wait for initial load
await page.waitForSelector('app-story-item');
// Store initial first story title
const initialTitle = await page.locator('app-story-item .story-title').first().textContent();

// Perform search
await page.fill('input[placeholder="Enter part of story title to search..."]', 'test');
// Perform search with a term we expect to find
const searchTerm = 'the'; // Common word that should appear in titles
await page.fill('input[placeholder="search in title ..."]', searchTerm);
await page.click('button:has-text("Search")');

// Wait for search results
await page.waitForSelector('.search-header');
// Wait for search header and verify search results
await expect(page.locator('.search-header')).toBeVisible({ timeout: 10000 });

// Verify search mode
expect(await page.locator('.search-header').isVisible()).toBeTruthy();
// Wait for and verify search results
await expect(async () => {
// Get all story titles
const titles = await page.locator('app-story-item .story-title').allTextContents();

// Verify we have results
expect(titles.length).toBeGreaterThan(0);

// Verify at least one title contains our search term (case insensitive)
const hasMatch = titles.some(title =>
title.toLowerCase().includes(searchTerm.toLowerCase())
);
expect(hasMatch).toBe(true);
}).toPass({ timeout: 30000 });

// Clear search
// Clear search and verify return to normal state
await page.click('button:has-text("Clear Search")');

// Verify back to normal mode
expect(await page.locator('.search-header').isVisible()).toBeFalsy();
await expect(page.locator('.search-header')).toBeHidden();
});

test('should handle loading states', async ({ page }) => {
// Wait for initial load
await page.waitForSelector('app-story-item', { timeout: 10000 });
// Store initial first story title
const initialTitle = await page.locator('app-story-item .story-title').first().textContent();

// Store initial stories for comparison
const initialStories = await page.locator('app-story-item').all();
const initialFirstStory = await page.locator('app-story-item').first().textContent();

// Click next page to trigger loading
// Click next page and wait for stories to change
await page.getByRole('button', { name: 'Next' }).click();

// Wait for stories to change (indicates loading completed)
await page.waitForFunction(
([initialText, initialLength]) => {
const stories = document.querySelectorAll('app-story-item');
const firstStory = stories[0]?.textContent;
return stories.length === initialLength && firstStory !== initialText;
},
[initialFirstStory, initialStories.length],
{ timeout: 10000 }
);
});

test('should handle search pagination', async ({ page }) => {
// Wait for initial load
await page.waitForSelector('app-story-item', { timeout: 10000 });

// Perform search
await page.fill('input[placeholder="Enter part of story title to search..."]', 'the');
await page.click('button:has-text("Search")');

// Wait for search results
await page.waitForSelector('.search-header');
// Wait for stories to change
await expect(async () => {
const newTitle = await page.locator('app-story-item .story-title').first().textContent();
expect(newTitle).not.toBe(initialTitle);
}).toPass({ timeout: 30000 });

// Wait for initial search results to load
await page.waitForSelector('app-story-item', { timeout: 10000 });

// Get initial count and content
const initialStories = await page.locator('app-story-item').all();
const initialCount = initialStories.length;
const initialFirstStory = await page.locator('app-story-item').first().textContent();

// Click next page if available
const nextButton = page.getByRole('button', { name: 'Next' });
if (await nextButton.isEnabled()) {
await nextButton.click();

// Wait for stories to change or count to change
await page.waitForFunction(
([initialText, initialLength]) => {
const stories = document.querySelectorAll('app-story-item');
const firstStory = stories[0]?.textContent;
return stories.length !== initialLength || firstStory !== initialText;
},
[initialFirstStory, initialCount],
{ timeout: 10000 }
);

// Get new count
const newStories = await page.locator('app-story-item').all();
expect(newStories.length).toBeLessThanOrEqual(initialCount);
}
await expect(page.locator('app-story-item')).toHaveCount(10);
});
});
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="story-item">
<div class="story-item" [attr.data-story-id]="story.id">
<span class="sequence-number">{{sequenceNumber}}.</span>
<a [href]="story.url" target="_blank" class="story-link">
<span class="story-title">{{story.title}}</span>
Expand Down

0 comments on commit 99097ad

Please sign in to comment.