Initial commit
This commit is contained in:
12
apps/shop-e2e/eslint.config.mjs
Normal file
12
apps/shop-e2e/eslint.config.mjs
Normal file
@@ -0,0 +1,12 @@
|
||||
import playwright from 'eslint-plugin-playwright';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
playwright.configs['flat/recommended'],
|
||||
...baseConfig,
|
||||
{
|
||||
files: ['**/*.ts', '**/*.js'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
70
apps/shop-e2e/playwright.config.ts
Normal file
70
apps/shop-e2e/playwright.config.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
import { nxE2EPreset } from '@nx/playwright/preset';
|
||||
import { workspaceRoot } from '@nx/devkit';
|
||||
|
||||
// For CI, you may want to set BASE_URL to the deployed application.
|
||||
const baseURL = process.env['BASE_URL'] || 'http://localhost:4200';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
...nxE2EPreset(__filename, { testDir: './src' }),
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
baseURL,
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
command: 'npx nx run shop:serve',
|
||||
url: 'http://localhost:4200',
|
||||
reuseExistingServer: true,
|
||||
cwd: workspaceRoot,
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
// Uncomment to test wider variety of browsers
|
||||
// Note, you will also need to update `.github/workflows/ci.yml`
|
||||
// Replacing `npx playwright install --only-shell` with `npx playwright install --with-deps`
|
||||
// {
|
||||
// name: 'firefox',
|
||||
// use: { ...devices['Desktop Firefox'] },
|
||||
// },
|
||||
|
||||
// {
|
||||
// name: 'webkit',
|
||||
// use: { ...devices['Desktop Safari'] },
|
||||
// },
|
||||
|
||||
// Uncomment for mobile browsers support
|
||||
/* {
|
||||
name: 'Mobile Chrome',
|
||||
use: { ...devices['Pixel 5'] },
|
||||
},
|
||||
{
|
||||
name: 'Mobile Safari',
|
||||
use: { ...devices['iPhone 12'] },
|
||||
}, */
|
||||
|
||||
// Uncomment for branded browsers
|
||||
/* {
|
||||
name: 'Microsoft Edge',
|
||||
use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
},
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
} */
|
||||
],
|
||||
});
|
||||
8
apps/shop-e2e/project.json
Normal file
8
apps/shop-e2e/project.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "shop-e2e",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "apps/shop-e2e/src",
|
||||
"implicitDependencies": ["shop"],
|
||||
"targets": {}
|
||||
}
|
||||
162
apps/shop-e2e/src/navigation-flow.spec.ts
Normal file
162
apps/shop-e2e/src/navigation-flow.spec.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Navigation and User Flow', () => {
|
||||
test('should complete a full user journey through the app', async ({ page }) => {
|
||||
// Start at home page
|
||||
await page.goto('/');
|
||||
|
||||
// Should redirect to products
|
||||
await page.waitForURL('**/products');
|
||||
|
||||
// Verify we're on products page
|
||||
const productsHeading = page.locator('h1:has-text("Our Products")');
|
||||
await expect(productsHeading).toBeVisible();
|
||||
|
||||
// Search for a specific product
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('Product 1');
|
||||
await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0);
|
||||
|
||||
// Click on the first search result
|
||||
const firstResult = page.locator('[class*="product-card"]').first();
|
||||
const productName = await firstResult.locator('h3').textContent();
|
||||
await firstResult.click();
|
||||
|
||||
// Should navigate to product detail
|
||||
await page.waitForURL('**/products/**');
|
||||
|
||||
// Verify product detail page
|
||||
const detailProductName = page.locator('h1').filter({ hasText: productName || '' });
|
||||
await expect(detailProductName).toBeVisible();
|
||||
|
||||
// Click back to products
|
||||
const backLink = page.locator('a:has-text("Back to Products")');
|
||||
await backLink.click();
|
||||
|
||||
// Should be back on products page
|
||||
await page.waitForURL('**/products');
|
||||
await expect(productsHeading).toBeVisible();
|
||||
|
||||
// The search should be cleared
|
||||
await expect(searchInput).toHaveValue('');
|
||||
});
|
||||
|
||||
test('should handle navigation via header link', async ({ page }) => {
|
||||
// Start on a product detail page
|
||||
await page.goto('/products/prod-1');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Click the Products link in the header
|
||||
const headerProductsLink = page.locator('nav a:has-text("Products")');
|
||||
await headerProductsLink.click();
|
||||
|
||||
// Should navigate to products listing
|
||||
await page.waitForURL('**/products');
|
||||
|
||||
// Verify products page is loaded
|
||||
const productsGrid = page.locator('[class*="product"]').first();
|
||||
await expect(productsGrid).toBeVisible();
|
||||
});
|
||||
|
||||
test('should maintain filter state during navigation', async ({ page }) => {
|
||||
// Go to products page
|
||||
await page.goto('/products');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Apply filters
|
||||
const categoryDropdown = page.locator('select');
|
||||
await categoryDropdown.selectOption('Electronics');
|
||||
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('Product');
|
||||
|
||||
// Wait for filtered results
|
||||
await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0);
|
||||
|
||||
// Navigate to a product
|
||||
const product = page.locator('[class*="product-card"]').first();
|
||||
await product.click();
|
||||
|
||||
// Wait for product detail page
|
||||
await page.waitForURL('**/products/**');
|
||||
|
||||
// Go back using browser back button
|
||||
await page.goBack();
|
||||
|
||||
// Filters should be reset (this is the expected behavior in most SPAs)
|
||||
// The search and category should be cleared
|
||||
await expect(searchInput).toHaveValue('');
|
||||
await expect(categoryDropdown).toHaveValue('');
|
||||
});
|
||||
|
||||
test('should handle rapid navigation', async ({ page }) => {
|
||||
// Navigate to products
|
||||
await page.goto('/products');
|
||||
|
||||
// Quickly click multiple products
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const product = page.locator('[class*="product-card"]').nth(i);
|
||||
await product.click();
|
||||
await page.waitForURL('**/products/**');
|
||||
|
||||
// Verify page loaded correctly
|
||||
const productDetail = page.locator('h1').nth(1);
|
||||
await expect(productDetail).toBeVisible();
|
||||
|
||||
// Go back
|
||||
const backLink = page.locator('a:has-text("Back to Products")');
|
||||
await backLink.click();
|
||||
await page.waitForURL('**/products');
|
||||
}
|
||||
|
||||
// Should still be functional
|
||||
const productsHeading = page.locator('h1:has-text("Our Products")');
|
||||
await expect(productsHeading).toBeVisible();
|
||||
});
|
||||
|
||||
test('should handle direct URL navigation', async ({ page }) => {
|
||||
// Navigate directly to a product detail page
|
||||
await page.goto('/products/prod-5');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Should load the correct product
|
||||
const productName = page.locator('h1').filter({ hasText: 'Product 5' });
|
||||
await expect(productName).toBeVisible();
|
||||
|
||||
// Navigate directly to products page
|
||||
await page.goto('/products');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Should show products listing
|
||||
const productsGrid = page.locator('[class*="product-card"]');
|
||||
const count = await productsGrid.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
|
||||
// Navigate to non-existent route should redirect to products
|
||||
await page.goto('/non-existent-route');
|
||||
await page.waitForURL('**/products');
|
||||
|
||||
// Should be on products page
|
||||
const productsHeading = page.locator('h1:has-text("Our Products")');
|
||||
await expect(productsHeading).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display loading states during navigation', async ({ page }) => {
|
||||
// Enable slow network to see loading states
|
||||
await page.route('**/*', route => {
|
||||
setTimeout(() => route.continue(), 100);
|
||||
});
|
||||
|
||||
await page.goto('/products');
|
||||
|
||||
// Click on a product
|
||||
const product = page.locator('[class*="product-card"]').first();
|
||||
await product.click();
|
||||
|
||||
// Should eventually load the product detail
|
||||
await page.waitForURL('**/products/**', { timeout: 10000 });
|
||||
|
||||
const productDetail = page.locator('[class*="product-detail"]');
|
||||
await expect(productDetail).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
});
|
||||
147
apps/shop-e2e/src/product-detail.spec.ts
Normal file
147
apps/shop-e2e/src/product-detail.spec.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Product Detail Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to products page first
|
||||
await page.goto('/products');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
});
|
||||
|
||||
test('should navigate to product detail when clicking a product', async ({ page }) => {
|
||||
// Click on the first product
|
||||
const firstProduct = page.locator('[class*="product-card"]').first();
|
||||
await firstProduct.click();
|
||||
|
||||
// Wait for navigation
|
||||
await page.waitForURL('**/products/**');
|
||||
|
||||
// Verify URL changed to product detail
|
||||
expect(page.url()).toMatch(/\/products\/prod-\d+/);
|
||||
|
||||
// Verify back link is visible
|
||||
const backLink = page.locator('a:has-text("Back to Products")');
|
||||
await expect(backLink).toBeVisible();
|
||||
});
|
||||
|
||||
test('should display complete product information', async ({ page }) => {
|
||||
// Navigate directly to a product detail page
|
||||
await page.goto('/products/prod-1');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Check product name
|
||||
const productName = page.locator('h1').filter({ hasText: /Product \d+/ });
|
||||
await expect(productName).toBeVisible();
|
||||
|
||||
// Check product image
|
||||
const productImage = page.locator('[class*="product-detail"] img');
|
||||
await expect(productImage).toBeVisible();
|
||||
await expect(productImage).toHaveAttribute('alt', /Product/);
|
||||
|
||||
// Check category
|
||||
const category = page.locator('text=/Home & Garden|Electronics|Clothing|Sports|Books/');
|
||||
await expect(category.first()).toBeVisible();
|
||||
|
||||
// Check rating
|
||||
const ratingSection = page.locator('[class*="product-rating"]');
|
||||
await expect(ratingSection).toBeVisible();
|
||||
const stars = ratingSection.locator('[class*="stars"]');
|
||||
await expect(stars).toBeVisible();
|
||||
|
||||
// Check price
|
||||
const price = page.locator('text=/\\$\\d+\\.\\d{2}/').first();
|
||||
await expect(price).toBeVisible();
|
||||
|
||||
// Check description
|
||||
const descriptionHeading = page.locator('h2:has-text("Description")');
|
||||
await expect(descriptionHeading).toBeVisible();
|
||||
const description = page.locator('p').filter({ hasText: /high-quality/ });
|
||||
await expect(description).toBeVisible();
|
||||
|
||||
// Check product information section
|
||||
const productInfoHeading = page.locator('h3:has-text("Product Information")');
|
||||
await expect(productInfoHeading).toBeVisible();
|
||||
|
||||
// Check product ID
|
||||
const productId = page.locator('text=/prod-\\d+/');
|
||||
await expect(productId).toBeVisible();
|
||||
|
||||
// Check availability - target the dd element in product info section to avoid matching the badge
|
||||
const availability = page.locator('dd').filter({ hasText: /In Stock|Out of Stock/ });
|
||||
await expect(availability).toBeVisible();
|
||||
});
|
||||
|
||||
test('should show action buttons for in-stock products', async ({ page }) => {
|
||||
// Navigate to products page
|
||||
await page.goto('/products');
|
||||
|
||||
// Find an in-stock product (one without "Out of Stock" badge)
|
||||
const inStockProduct = page.locator('[class*="product-card"]').filter({ hasNot: page.locator('text="Out of Stock"') }).first();
|
||||
await inStockProduct.click();
|
||||
|
||||
// Wait for product detail page
|
||||
await page.waitForURL('**/products/**');
|
||||
|
||||
// Check for Add to Cart button
|
||||
const addToCartButton = page.locator('button:has-text("Add to Cart")');
|
||||
await expect(addToCartButton).toBeVisible();
|
||||
await expect(addToCartButton).toBeEnabled();
|
||||
|
||||
// Check for Add to Wishlist button
|
||||
const addToWishlistButton = page.locator('button:has-text("Add to Wishlist")');
|
||||
await expect(addToWishlistButton).toBeVisible();
|
||||
await expect(addToWishlistButton).toBeEnabled();
|
||||
|
||||
// Test clicking Add to Cart (should show alert)
|
||||
page.on('dialog', dialog => {
|
||||
expect(dialog.message()).toContain('Product added to cart');
|
||||
dialog.accept();
|
||||
});
|
||||
await addToCartButton.click();
|
||||
});
|
||||
|
||||
test('should show disabled button for out-of-stock products', async ({ page }) => {
|
||||
// Navigate to products page
|
||||
await page.goto('/products');
|
||||
|
||||
// Find an out-of-stock product. The dataset of 50 products marks ~20% as
|
||||
// out of stock, so at least one is always present.
|
||||
const outOfStockProduct = page.locator('[class*="product-card"]').filter({ has: page.locator('text="Out of Stock"') }).first();
|
||||
await expect(outOfStockProduct).toBeVisible();
|
||||
|
||||
await outOfStockProduct.click();
|
||||
|
||||
// Wait for product detail page
|
||||
await page.waitForURL('**/products/**');
|
||||
|
||||
// Check for out of stock badge on detail page - use more specific selector to avoid strict mode violation
|
||||
const outOfStockBadge = page.locator('.out-of-stock-badge').filter({ hasText: 'Out of Stock' });
|
||||
await expect(outOfStockBadge).toBeVisible();
|
||||
|
||||
// Check for disabled button
|
||||
const unavailableButton = page.locator('button:has-text("Currently Unavailable")');
|
||||
await expect(unavailableButton).toBeVisible();
|
||||
await expect(unavailableButton).toBeDisabled();
|
||||
|
||||
// Should not have Add to Cart or Wishlist buttons
|
||||
const addToCartButton = page.locator('button:has-text("Add to Cart")');
|
||||
await expect(addToCartButton).toBeHidden();
|
||||
});
|
||||
|
||||
test('should navigate back to products list', async ({ page }) => {
|
||||
// Navigate to a product detail page
|
||||
await page.goto('/products/prod-1');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
|
||||
// Click the back link
|
||||
const backLink = page.locator('a:has-text("Back to Products")');
|
||||
await backLink.click();
|
||||
|
||||
// Should navigate back to products page
|
||||
await page.waitForURL('**/products');
|
||||
expect(page.url()).toContain('/products');
|
||||
|
||||
// Products grid should be visible
|
||||
const productsGrid = page.locator('[class*="product-grid"], [class*="products"]');
|
||||
await expect(productsGrid).toBeVisible();
|
||||
});
|
||||
});
|
||||
146
apps/shop-e2e/src/product-listing.spec.ts
Normal file
146
apps/shop-e2e/src/product-listing.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Product Listing Page', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/products');
|
||||
await page.waitForLoadState('domcontentloaded');
|
||||
});
|
||||
|
||||
test('should display products grid with at least one product', async ({ page }) => {
|
||||
// Check that products are displayed
|
||||
const productCards = page.locator('[class*="product-card"]');
|
||||
const count = await productCards.count();
|
||||
expect(count).toBeGreaterThan(0);
|
||||
|
||||
// Check first product has required elements
|
||||
const firstProduct = productCards.first();
|
||||
await expect(firstProduct).toBeVisible();
|
||||
|
||||
// Check for product image
|
||||
const productImage = firstProduct.locator('img');
|
||||
await expect(productImage).toBeVisible();
|
||||
|
||||
// Check for product name (heading level 3)
|
||||
const productName = firstProduct.locator('h3');
|
||||
await expect(productName).toBeVisible();
|
||||
|
||||
// Check for price
|
||||
const productPrice = firstProduct.locator('text=/\\$\\d+\\.\\d{2}/');
|
||||
await expect(productPrice).toBeVisible();
|
||||
});
|
||||
|
||||
test('should filter products by category', async ({ page }) => {
|
||||
// Select a specific category from dropdown
|
||||
const categoryDropdown = page.locator('select');
|
||||
await categoryDropdown.selectOption('Electronics');
|
||||
|
||||
// Wait for products to load
|
||||
await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0);
|
||||
|
||||
// Verify all visible products are from Electronics category
|
||||
const productCategories = page.locator('[class*="product-card"] p:first-of-type');
|
||||
const count = await productCategories.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const category = productCategories.nth(i);
|
||||
await expect(category).toHaveText('Electronics');
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter products by search term', async ({ page }) => {
|
||||
// Enter search term
|
||||
const searchInput = page.locator('input[placeholder*="Search"]');
|
||||
await searchInput.fill('Product 1');
|
||||
|
||||
// Wait for debounce and results
|
||||
await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0);
|
||||
|
||||
// Check that filtered products contain the search term
|
||||
const productNames = page.locator('[class*="product-card"] h3');
|
||||
const count = await productNames.count();
|
||||
|
||||
for (let i = 0; i < count; i++) {
|
||||
const name = await productNames.nth(i).textContent();
|
||||
expect(name?.toLowerCase()).toContain('product 1');
|
||||
}
|
||||
});
|
||||
|
||||
test('should filter by in-stock products only', async ({ page }) => {
|
||||
// Get initial product count
|
||||
const resultsInfo = page.locator('text=/Showing \\d+ of \\d+ products/');
|
||||
const initialText = await resultsInfo.textContent();
|
||||
|
||||
// Click the "In Stock Only" checkbox
|
||||
const inStockCheckbox = page.locator('input[type="checkbox"]');
|
||||
await inStockCheckbox.check();
|
||||
|
||||
// Wait for the results to update (the total count should change)
|
||||
await page.waitForFunction(
|
||||
(initialText) => {
|
||||
const resultsElement = document.querySelector('[class*="results-info"]');
|
||||
return resultsElement && resultsElement.textContent !== initialText;
|
||||
},
|
||||
initialText,
|
||||
{ timeout: 5000 }
|
||||
);
|
||||
|
||||
// Verify no "Out of Stock" badges are visible (web-first wait for the
|
||||
// filtered DOM to settle)
|
||||
const outOfStockBadges = page.locator('text="Out of Stock"');
|
||||
await expect(outOfStockBadges).toHaveCount(0);
|
||||
});
|
||||
|
||||
test('should handle pagination', async ({ page }) => {
|
||||
// Check that pagination controls exist
|
||||
const paginationSection = page.locator('[class*="pagination"]');
|
||||
await expect(paginationSection).toBeVisible();
|
||||
|
||||
// Check initial state - Previous should be disabled, Next should be enabled
|
||||
const prevButton = page.locator('button:has-text("Previous")');
|
||||
const nextButton = page.locator('button:has-text("Next")');
|
||||
const pageInfo = page.locator('text=/Page \\d+ of \\d+/');
|
||||
|
||||
await expect(prevButton).toBeDisabled();
|
||||
await expect(nextButton).toBeEnabled();
|
||||
await expect(pageInfo).toBeVisible();
|
||||
|
||||
// Click next button
|
||||
await nextButton.click();
|
||||
|
||||
// Wait for new products to load
|
||||
await page.waitForFunction(() => document.querySelectorAll('[class*="product-card"]').length > 0);
|
||||
|
||||
// Verify page changed
|
||||
const newPageInfo = await pageInfo.textContent();
|
||||
expect(newPageInfo).toContain('Page 2');
|
||||
|
||||
// Now Previous should be enabled
|
||||
await expect(prevButton).toBeEnabled();
|
||||
});
|
||||
|
||||
test('should show correct product count', async ({ page }) => {
|
||||
// Check the results info text
|
||||
const resultsInfo = page.locator('text=/Showing \\d+ of \\d+ products/');
|
||||
await expect(resultsInfo).toBeVisible();
|
||||
|
||||
// Get the count from the text. The visibility assertion above guarantees
|
||||
// the "Showing X of Y" text is present, so the digits are deterministic.
|
||||
const text = String(await resultsInfo.textContent());
|
||||
const numbers = Array.from(text.matchAll(/\d+/g), (m) => m[0]);
|
||||
expect(numbers).toHaveLength(2);
|
||||
|
||||
const [showingText, totalText] = numbers;
|
||||
const showing = parseInt(showingText);
|
||||
const total = parseInt(totalText);
|
||||
|
||||
// Verify we're showing reasonable numbers
|
||||
expect(showing).toBeGreaterThan(0);
|
||||
expect(showing).toBeLessThanOrEqual(12); // Default page size
|
||||
expect(total).toBeGreaterThan(0);
|
||||
|
||||
// Count actual product cards
|
||||
const productCards = page.locator('[class*="product-card"]');
|
||||
const actualCount = await productCards.count();
|
||||
expect(actualCount).toBe(showing);
|
||||
});
|
||||
});
|
||||
46
apps/shop-e2e/src/shop-homepage.spec.ts
Normal file
46
apps/shop-e2e/src/shop-homepage.spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Shop Homepage', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
});
|
||||
|
||||
test('should display the main header and navigation', async ({ page }) => {
|
||||
// Check header title
|
||||
const header = page.locator('h1').first();
|
||||
await expect(header).toContainText('Nx Shop Demo');
|
||||
|
||||
// Check navigation link exists
|
||||
const productsLink = page.locator('nav a:has-text("Products")');
|
||||
await expect(productsLink).toBeVisible();
|
||||
await expect(productsLink).toHaveAttribute('href', '/products');
|
||||
});
|
||||
|
||||
test('should display footer with copyright information', async ({ page }) => {
|
||||
// Check footer content
|
||||
const footer = page.locator('footer');
|
||||
await expect(footer).toContainText('© 2025 Nx Shop Demo');
|
||||
await expect(footer).toContainText(
|
||||
'Frontend (Angular) + Backend (Express) + Shared Libraries'
|
||||
);
|
||||
});
|
||||
|
||||
test('should redirect to products page by default', async ({ page }) => {
|
||||
// The app should redirect from / to /products
|
||||
await page.waitForURL('**/products');
|
||||
expect(page.url()).toContain('/products');
|
||||
|
||||
// Verify we're on the products page
|
||||
const productsHeading = page.locator('h1:has-text("Our Products")');
|
||||
await expect(productsHeading).toBeVisible();
|
||||
});
|
||||
|
||||
test('should have proper page title and meta', async ({ page }) => {
|
||||
// Check page title
|
||||
await expect(page).toHaveTitle(/shop/i);
|
||||
|
||||
// Check viewport is responsive
|
||||
const viewport = page.viewportSize();
|
||||
expect(viewport).toBeTruthy();
|
||||
});
|
||||
});
|
||||
25
apps/shop-e2e/tsconfig.json
Normal file
25
apps/shop-e2e/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"sourceMap": false,
|
||||
"module": "commonjs",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": [
|
||||
"**/*.ts",
|
||||
"**/*.js",
|
||||
"playwright.config.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user