initial commit after project creation
This commit is contained in:
173
tests/e2e/README.md
Normal file
173
tests/e2e/README.md
Normal file
@@ -0,0 +1,173 @@
|
||||
# Search Functionality Tests
|
||||
|
||||
This directory contains Playwright end-to-end tests for the search functionality in TinaDocs.
|
||||
|
||||
## Overview
|
||||
|
||||
The search tests verify that the Pagefind-based search functionality works correctly across different scenarios:
|
||||
|
||||
- Basic search functionality
|
||||
- Search result display
|
||||
- Navigation to search results
|
||||
- Mobile responsiveness
|
||||
- Performance testing
|
||||
- Error handling
|
||||
|
||||
## Test Files
|
||||
|
||||
- `search.spec.ts` - Comprehensive search tests with detailed scenarios
|
||||
- `search-simplified.spec.ts` - Simplified tests using helper utilities
|
||||
- `utils/search-helpers.ts` - Helper functions for search testing
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Local Development
|
||||
|
||||
1. **Start the development server:**
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
2. **Run tests with UI (for debugging):**
|
||||
```bash
|
||||
pnpm test:ui
|
||||
```
|
||||
|
||||
### Preview/Production Testing
|
||||
|
||||
1. **Test against a specific URL:**
|
||||
```bash
|
||||
PREVIEW_URL=https://your-preview-url.com pnpm test
|
||||
```
|
||||
|
||||
2. **Run all tests:**
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Test Scenarios
|
||||
|
||||
### Basic Functionality
|
||||
- ✅ Search input field is visible
|
||||
- ✅ Search returns results for existing content
|
||||
- ✅ Search shows "No Llamas Found" for non-existent content
|
||||
- ✅ Search clears when clicking outside
|
||||
- ✅ Empty search input is handled gracefully
|
||||
- ✅ Multiple rapid searches work correctly
|
||||
|
||||
### Technical Verification
|
||||
- ✅ Pagefind files are accessible (`/_next/static/pagefind/`)
|
||||
- ✅ Search completes within reasonable time (< 3 seconds)
|
||||
- ✅ Mobile viewport works correctly
|
||||
|
||||
### Performance
|
||||
- ✅ Search performance is measured and reported
|
||||
|
||||
## GitHub Actions Integration
|
||||
|
||||
The tests are automatically run in GitHub Actions on:
|
||||
|
||||
1. **Pull Requests** - Tests run against Vercel preview deployments
|
||||
2. **Manual Workflow** - Can test against any preview URL
|
||||
|
||||
## Configuration
|
||||
|
||||
### Environment Variables
|
||||
|
||||
- `BASE_URL` - Base URL for testing (defaults to `http://localhost:3000`)
|
||||
- `PREVIEW_URL` - Preview URL for testing
|
||||
|
||||
### Playwright Configuration
|
||||
|
||||
See `playwright.config.ts` for:
|
||||
- Browser configurations (Chrome)
|
||||
- Test timeouts and retries
|
||||
- Screenshot and video capture settings
|
||||
- Report generation
|
||||
|
||||
## Debugging
|
||||
|
||||
### View Test Reports
|
||||
|
||||
After running tests, view the HTML report:
|
||||
```bash
|
||||
npx playwright show-report
|
||||
```
|
||||
|
||||
### Debug Individual Tests
|
||||
|
||||
1. Add `test.only()` to run a single test
|
||||
2. Use `--debug` flag for step-by-step debugging
|
||||
3. Use `--headed` to see the browser in action
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Search not working locally:**
|
||||
- Ensure Pagefind index is generated: `pnpm build-local-pagefind`
|
||||
- Check if `/_next/static/pagefind/` files exist
|
||||
|
||||
2. **Tests timing out:**
|
||||
- Increase timeout in `playwright.config.ts`
|
||||
- Check if the server is running and accessible
|
||||
|
||||
3. **Search results not appearing:**
|
||||
- Verify search input selector matches the actual component
|
||||
- Check if Pagefind files are being served correctly
|
||||
|
||||
## Test Data
|
||||
|
||||
The tests use predefined search terms that should exist in your documentation:
|
||||
|
||||
```typescript
|
||||
const KNOWN_CONTENT = {
|
||||
searchTerms: [
|
||||
'TinaDocs',
|
||||
'documentation',
|
||||
'search',
|
||||
'API',
|
||||
'TinaCMS',
|
||||
'deployment',
|
||||
'theming',
|
||||
'components'
|
||||
]
|
||||
};
|
||||
```
|
||||
|
||||
**Important:** Update these terms based on your actual content to ensure tests pass reliably.
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new search features:
|
||||
|
||||
1. Add corresponding tests to `search.spec.ts`
|
||||
2. Update helper functions in `search-helpers.ts` if needed
|
||||
3. Update test data with relevant search terms
|
||||
4. Ensure tests pass locally before committing
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Search Index Issues
|
||||
|
||||
If search isn't working, check:
|
||||
|
||||
1. **Build process:**
|
||||
```bash
|
||||
pnpm build-local-pagefind
|
||||
```
|
||||
|
||||
2. **Pagefind files:**
|
||||
- `/_next/static/pagefind/pagefind.js`
|
||||
- `/_next/static/pagefind/pagefind-index.json`
|
||||
|
||||
3. **Content indexing:**
|
||||
- Ensure content has `data-pagefind-body` attributes
|
||||
- Check if content is being built correctly
|
||||
|
||||
### Test Failures
|
||||
|
||||
Common test failure reasons:
|
||||
|
||||
1. **Selector changes** - Update selectors in tests
|
||||
2. **Content changes** - Update test data with new content
|
||||
3. **Timing issues** - Increase wait times or add better wait conditions
|
||||
4. **Environment issues** - Check if server is running and accessible
|
||||
166
tests/e2e/search.spec.ts
Normal file
166
tests/e2e/search.spec.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { SEARCH_TEST_DATA, SearchHelper } from "./utils/search-helpers";
|
||||
|
||||
// Test data for known content that should be searchable
|
||||
const KNOWN_CONTENT = {
|
||||
// These should be updated based on your actual content
|
||||
searchTerms: [
|
||||
"TinaDocs",
|
||||
"documentation",
|
||||
"search",
|
||||
"API",
|
||||
"TinaCMS",
|
||||
"deployment",
|
||||
"theming",
|
||||
"components",
|
||||
],
|
||||
nonExistentTerms: [
|
||||
"xyz123nonexistent",
|
||||
"completelyrandomterm",
|
||||
"shouldnotexist",
|
||||
],
|
||||
};
|
||||
|
||||
test.describe("Search Functionality", () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to the docs page before each test
|
||||
await page.goto(`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/docs`);
|
||||
|
||||
// Wait for the page to load completely
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Wait for the search input to be available (client component hydration)
|
||||
const searchInput = page.locator('input[placeholder="Search..."]');
|
||||
await searchInput.waitFor({ state: "visible", timeout: 10000 });
|
||||
});
|
||||
|
||||
test("should show search results for existing content", async ({ page }) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
// Test with a known search term
|
||||
await searchHelper.performSearch(SEARCH_TEST_DATA.knownTerms[0]);
|
||||
|
||||
// Check if search results container is visible
|
||||
await searchHelper.expectSearchResultsVisible();
|
||||
|
||||
// Verify that results are clickable links
|
||||
const resultLinks = searchHelper.getSearchResultLinks();
|
||||
await expect(resultLinks.first()).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show "No Llamas Found" for non-existent content', async ({
|
||||
page,
|
||||
}) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
// Test with a non-existent search term
|
||||
await searchHelper.performSearch(SEARCH_TEST_DATA.nonExistentTerms[0]);
|
||||
|
||||
// Check if "No Llamas Found" message appears
|
||||
const noResultsMessage = searchHelper.getNoResultsMessage();
|
||||
await expect(noResultsMessage).toBeVisible();
|
||||
});
|
||||
|
||||
test("should clear search results when clicking outside", async ({
|
||||
page,
|
||||
}) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
// Perform a search
|
||||
await searchHelper.performSearch(SEARCH_TEST_DATA.knownTerms[0]);
|
||||
|
||||
// Click outside the search area
|
||||
await searchHelper.clearSearch();
|
||||
|
||||
// Verify search results are cleared
|
||||
await searchHelper.expectSearchResultsNotVisible();
|
||||
|
||||
// Verify search input is cleared
|
||||
await searchHelper.expectSearchInputValue("");
|
||||
});
|
||||
|
||||
test("should handle empty search input", async ({ page }) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
// Try to search with empty input
|
||||
await searchHelper.performSearch("");
|
||||
|
||||
// Verify no search results are shown
|
||||
await searchHelper.expectSearchResultsNotVisible();
|
||||
});
|
||||
|
||||
test("should navigate to search result pages", async ({ page }) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
// Perform a search
|
||||
await searchHelper.performSearch(SEARCH_TEST_DATA.knownTerms[0]);
|
||||
|
||||
// Click on the first search result
|
||||
const firstResult = searchHelper.getSearchResultLinks().first();
|
||||
await expect(firstResult).toBeVisible();
|
||||
|
||||
// Store the href to verify navigation
|
||||
const href = await firstResult.getAttribute("href");
|
||||
expect(href).toBeTruthy();
|
||||
|
||||
// Click the result
|
||||
await firstResult.click();
|
||||
|
||||
// Verify navigation occurred
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Check if we're on a docs page
|
||||
await expect(page).toHaveURL(/\/docs/);
|
||||
});
|
||||
|
||||
test("should show loading state during search", async ({ page }) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
// Start typing to trigger search
|
||||
const searchInput = searchHelper.getSearchInput();
|
||||
// Ensure the input is visible before interacting
|
||||
await searchInput.waitFor({ state: "visible", timeout: 10000 });
|
||||
await searchInput.fill(SEARCH_TEST_DATA.knownTerms[0]);
|
||||
|
||||
// Check for loading indicator (if implemented)
|
||||
// This might show "Mustering all the Llamas..." message
|
||||
const loadingMessage = searchHelper.getLoadingMessage();
|
||||
|
||||
// The loading state might be very brief, so we'll just verify the search works
|
||||
await searchInput.press("Enter");
|
||||
|
||||
// Verify search completed (either with results or no results message)
|
||||
await searchHelper.expectSearchResultsVisible();
|
||||
});
|
||||
|
||||
test("should verify Pagefind files are accessible", async ({ page }) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
await searchHelper.verifyPagefindFilesAccessible();
|
||||
});
|
||||
|
||||
test("should work on mobile viewport", async ({ page }) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
await searchHelper.testMobileSearch();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Search Performance", () => {
|
||||
test("should complete search within reasonable time", async ({ page }) => {
|
||||
const searchHelper = new SearchHelper(page);
|
||||
|
||||
await searchHelper.navigateToDocs();
|
||||
|
||||
// Measure search performance
|
||||
const searchTime = await searchHelper.measureSearchPerformance(
|
||||
SEARCH_TEST_DATA.knownTerms[0]
|
||||
);
|
||||
|
||||
// Search should complete within 3 seconds
|
||||
expect(searchTime).toBeLessThan(3000);
|
||||
|
||||
// Verify search completed successfully
|
||||
await searchHelper.expectSearchResultsVisible();
|
||||
});
|
||||
});
|
||||
218
tests/e2e/utils/search-helpers.ts
Normal file
218
tests/e2e/utils/search-helpers.ts
Normal file
@@ -0,0 +1,218 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
export class SearchHelper {
|
||||
constructor(private page: Page) {}
|
||||
|
||||
/**
|
||||
* Navigate to the docs page and wait for it to load
|
||||
*/
|
||||
async navigateToDocs() {
|
||||
await this.page.goto(`${process.env.NEXT_PUBLIC_BASE_PATH ?? ""}/docs`);
|
||||
await this.page.waitForLoadState("networkidle");
|
||||
// Wait for the search input to be available (client component hydration)
|
||||
const searchInput = this.getSearchInput();
|
||||
await searchInput.waitFor({ state: "attached", timeout: 10000 });
|
||||
}
|
||||
|
||||
/**
|
||||
* Find and return the search input field
|
||||
*/
|
||||
getSearchInput() {
|
||||
return this.page.locator('input[placeholder="Search..."]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform a search with the given term
|
||||
*/
|
||||
async performSearch(searchTerm: string) {
|
||||
const searchInput = this.getSearchInput();
|
||||
// Wait for the search input to be visible and attached to the DOM
|
||||
// This is important because the Search component is a client component that needs to hydrate
|
||||
await searchInput.waitFor({ state: "visible", timeout: 10000 });
|
||||
await searchInput.fill(searchTerm);
|
||||
await searchInput.press("Enter");
|
||||
|
||||
// Wait for search to complete
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait for search results to appear
|
||||
*/
|
||||
async waitForSearchResults() {
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get search results container
|
||||
*/
|
||||
getSearchResultsContainer() {
|
||||
// Use the data-testid attribute for reliable selection
|
||||
return this.page.locator('[data-testid="search-results-container"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get "No Llamas Found" message
|
||||
*/
|
||||
getNoResultsMessage() {
|
||||
// Use the data-testid attribute for reliable selection
|
||||
return this.page.locator('[data-testid="no-results-message"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get loading message
|
||||
*/
|
||||
getLoadingMessage() {
|
||||
// Look for the loading message within the search results container
|
||||
return this.page.locator(
|
||||
'[data-testid="search-results-container"] h4:has-text("Mustering all the Llamas")'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all search result links
|
||||
*/
|
||||
getSearchResultLinks() {
|
||||
return this.page.locator('a[href*="/docs"]');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify search results are visible
|
||||
*/
|
||||
async expectSearchResultsVisible() {
|
||||
const resultsContainer = this.getSearchResultsContainer();
|
||||
const noResultsMessage = this.getNoResultsMessage();
|
||||
|
||||
await expect(resultsContainer.or(noResultsMessage)).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify search results are not visible
|
||||
*/
|
||||
async expectSearchResultsNotVisible() {
|
||||
const resultsContainer = this.getSearchResultsContainer();
|
||||
await expect(resultsContainer).not.toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify search input is visible
|
||||
*/
|
||||
async expectSearchInputVisible() {
|
||||
const searchInput = this.getSearchInput();
|
||||
await expect(searchInput).toBeVisible();
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify search input has the expected value
|
||||
*/
|
||||
async expectSearchInputValue(expectedValue: string) {
|
||||
const searchInput = this.getSearchInput();
|
||||
await expect(searchInput).toHaveValue(expectedValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear search by clicking the copy button
|
||||
*/
|
||||
async clearSearch() {
|
||||
await this.page.click('button:has-text("Copy")');
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Pagefind files are accessible
|
||||
*/
|
||||
async verifyPagefindFilesAccessible() {
|
||||
const isDev = this.page.url().includes("localhost");
|
||||
|
||||
// Check Pagefind JavaScript file
|
||||
const pagefindJsResponse = await this.page.request.get(
|
||||
isDev
|
||||
? "http://localhost:3000/pagefind/pagefind.js"
|
||||
: `${process.env.BASE_URL}${
|
||||
process.env.NEXT_PUBLIC_BASE_PATH ?? ""
|
||||
}/_next/static/pagefind/pagefind.js`
|
||||
);
|
||||
expect(pagefindJsResponse.status()).toBe(200);
|
||||
|
||||
// Check Pagefind index file
|
||||
const pagefindIndexResponse = await this.page.request.get(
|
||||
isDev
|
||||
? "http://localhost:3000/pagefind/pagefind-ui.js"
|
||||
: `${process.env.BASE_URL}${
|
||||
process.env.NEXT_PUBLIC_BASE_PATH ?? ""
|
||||
}/_next/static/pagefind/pagefind-ui.js`
|
||||
);
|
||||
|
||||
expect(pagefindIndexResponse.status()).toBe(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure search performance
|
||||
*/
|
||||
async measureSearchPerformance(searchTerm: string): Promise<number> {
|
||||
const startTime = Date.now();
|
||||
|
||||
await this.performSearch(searchTerm);
|
||||
|
||||
const endTime = Date.now();
|
||||
return endTime - startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test search with multiple terms
|
||||
*/
|
||||
async testMultipleSearches(searchTerms: string[]) {
|
||||
for (const term of searchTerms) {
|
||||
await this.performSearch(term);
|
||||
await this.expectSearchResultsVisible();
|
||||
await this.clearSearch();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test search on mobile viewport
|
||||
*/
|
||||
async testMobileSearch() {
|
||||
await this.page.setViewportSize({ width: 375, height: 667 });
|
||||
await this.expectSearchInputVisible();
|
||||
|
||||
await this.performSearch("TinaDocs");
|
||||
await this.expectSearchResultsVisible();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test data for search tests
|
||||
*/
|
||||
export const SEARCH_TEST_DATA = {
|
||||
knownTerms: [
|
||||
"TinaDocs",
|
||||
"documentation",
|
||||
"search",
|
||||
"API",
|
||||
"TinaCMS",
|
||||
"deployment",
|
||||
"theming",
|
||||
"components",
|
||||
],
|
||||
nonExistentTerms: [
|
||||
"xyz123nonexistent",
|
||||
"completelyrandomterm",
|
||||
"shouldnotexist",
|
||||
],
|
||||
specialCharacters: [
|
||||
"@#$%",
|
||||
"test@example.com",
|
||||
"user-name",
|
||||
"file/path",
|
||||
"test&query",
|
||||
"test+query",
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a search helper instance
|
||||
*/
|
||||
export function createSearchHelper(page: Page): SearchHelper {
|
||||
return new SearchHelper(page);
|
||||
}
|
||||
Reference in New Issue
Block a user