chore: copia de libs/shared con prefijo Pi y APIs de terceros actualizadas
This commit is contained in:
@@ -1,22 +0,0 @@
|
||||
# This file is generated by Nx.
|
||||
# Build the docker image with `npx nx docker:build api`.
|
||||
# Tip: Modify "docker:build" options in project.json to change docker build args.
|
||||
#
|
||||
# Run the container with `nx docker:run api -p 3000:3000`.
|
||||
#
|
||||
FROM docker.io/node:lts-alpine
|
||||
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3333
|
||||
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY dist .
|
||||
|
||||
# You can remove this install step if you build with `--bundle` option.
|
||||
# The bundled output will include external dependencies.
|
||||
|
||||
RUN npm --omit=dev -f install
|
||||
|
||||
CMD [ "node", "main.js" ]
|
||||
@@ -1,3 +0,0 @@
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [...baseConfig];
|
||||
@@ -1,73 +0,0 @@
|
||||
{
|
||||
"name": "api",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "apps/api/src",
|
||||
"projectType": "application",
|
||||
"tags": ["scope:api"],
|
||||
"release": {
|
||||
"docker": {
|
||||
"repositoryName": "org/angular-template-api"
|
||||
}
|
||||
},
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@nx/esbuild:esbuild",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"defaultConfiguration": "production",
|
||||
"options": {
|
||||
"platform": "node",
|
||||
"outputPath": "apps/api/dist",
|
||||
"format": ["cjs"],
|
||||
"bundle": false,
|
||||
"main": "apps/api/src/main.ts",
|
||||
"tsConfig": "apps/api/tsconfig.app.json",
|
||||
"assets": ["apps/api/src/assets"],
|
||||
"generatePackageJson": true,
|
||||
"esbuildOptions": {
|
||||
"sourcemap": true,
|
||||
"outExtension": {
|
||||
".js": ".js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"configurations": {
|
||||
"development": {},
|
||||
"production": {
|
||||
"generateLockfile": true,
|
||||
"esbuildOptions": {
|
||||
"sourcemap": false,
|
||||
"outExtension": {
|
||||
".js": ".js"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"continuous": true,
|
||||
"executor": "@nx/js:node",
|
||||
"defaultConfiguration": "development",
|
||||
"dependsOn": ["build"],
|
||||
"options": {
|
||||
"buildTarget": "api:build",
|
||||
"runBuildTargetDependencies": false
|
||||
},
|
||||
"configurations": {
|
||||
"development": {
|
||||
"buildTarget": "api:build:development"
|
||||
},
|
||||
"production": {
|
||||
"buildTarget": "api:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"options": {
|
||||
"passWithNoTests": true
|
||||
}
|
||||
},
|
||||
"docker:build": {
|
||||
"dependsOn": ["build"]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
import express from 'express';
|
||||
import { ProductsService } from '@org/api/products';
|
||||
import { ApiResponse, Product, ProductFilter, PaginatedResponse } from '@org/models';
|
||||
|
||||
const host = process.env.HOST ?? 'localhost';
|
||||
const port = process.env.PORT ? Number(process.env.PORT) : 3333;
|
||||
|
||||
const app = express();
|
||||
const productsService = new ProductsService();
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
|
||||
// CORS configuration for Angular app
|
||||
app.use((req, res, next) => {
|
||||
res.header('Access-Control-Allow-Origin', '*');
|
||||
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(200);
|
||||
} else {
|
||||
next();
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/', (req, res) => {
|
||||
res.send({ message: 'Hello API' });
|
||||
});
|
||||
|
||||
// Products endpoints
|
||||
app.get('/api/products', (req, res) => {
|
||||
try {
|
||||
const filter: ProductFilter = {};
|
||||
|
||||
if (req.query.category) {
|
||||
filter.category = req.query.category as string;
|
||||
}
|
||||
if (req.query.minPrice) {
|
||||
filter.minPrice = Number(req.query.minPrice);
|
||||
}
|
||||
if (req.query.maxPrice) {
|
||||
filter.maxPrice = Number(req.query.maxPrice);
|
||||
}
|
||||
if (req.query.inStock !== undefined) {
|
||||
filter.inStock = req.query.inStock === 'true';
|
||||
}
|
||||
if (req.query.searchTerm) {
|
||||
filter.searchTerm = req.query.searchTerm as string;
|
||||
}
|
||||
|
||||
const page = req.query.page ? Number(req.query.page) : 1;
|
||||
const pageSize = req.query.pageSize ? Number(req.query.pageSize) : 12;
|
||||
|
||||
const result = productsService.getAllProducts(filter, page, pageSize);
|
||||
|
||||
const response: ApiResponse<PaginatedResponse<Product>> = {
|
||||
data: result,
|
||||
success: true,
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse<null> = {
|
||||
data: null,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/products/:id', (req, res) => {
|
||||
try {
|
||||
const product = productsService.getProductById(req.params.id);
|
||||
|
||||
if (!product) {
|
||||
const response: ApiResponse<null> = {
|
||||
data: null,
|
||||
success: false,
|
||||
error: 'Product not found',
|
||||
};
|
||||
return res.status(404).json(response);
|
||||
}
|
||||
|
||||
const response: ApiResponse<Product> = {
|
||||
data: product,
|
||||
success: true,
|
||||
};
|
||||
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse<null> = {
|
||||
data: null,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/products-metadata/categories', (req, res) => {
|
||||
try {
|
||||
const categories = productsService.getCategories();
|
||||
const response: ApiResponse<string[]> = {
|
||||
data: categories,
|
||||
success: true,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse<null> = {
|
||||
data: null,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/products-metadata/price-range', (req, res) => {
|
||||
try {
|
||||
const priceRange = productsService.getPriceRange();
|
||||
const response: ApiResponse<{ min: number; max: number }> = {
|
||||
data: priceRange,
|
||||
success: true,
|
||||
};
|
||||
res.json(response);
|
||||
} catch (error) {
|
||||
const response: ApiResponse<null> = {
|
||||
data: null,
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : 'Unknown error',
|
||||
};
|
||||
res.status(500).json(response);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(port, host, () => {
|
||||
console.log(`[ ready ] http://${host}:${port}`);
|
||||
});
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
],
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/api',
|
||||
plugins: [tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
test: {
|
||||
name: 'api',
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'node',
|
||||
include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
reporters: ['default'],
|
||||
passWithNoTests: true,
|
||||
coverage: {
|
||||
reportsDirectory: '../../coverage/apps/api',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
@@ -1,12 +0,0 @@
|
||||
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: {},
|
||||
},
|
||||
];
|
||||
@@ -1,70 +0,0 @@
|
||||
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' },
|
||||
} */
|
||||
],
|
||||
});
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "shop-e2e",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"sourceRoot": "apps/shop-e2e/src",
|
||||
"implicitDependencies": ["shop"],
|
||||
"targets": {}
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
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 });
|
||||
});
|
||||
});
|
||||
@@ -1,147 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,146 +0,0 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"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"
|
||||
]
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'app',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'app',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
@@ -1,88 +0,0 @@
|
||||
{
|
||||
"name": "shop",
|
||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
||||
"projectType": "application",
|
||||
"prefix": "app",
|
||||
"sourceRoot": "apps/shop/src",
|
||||
"tags": ["scope:shop"],
|
||||
"targets": {
|
||||
"build": {
|
||||
"executor": "@angular/build:application",
|
||||
"outputs": ["{options.outputPath}"],
|
||||
"options": {
|
||||
"outputPath": "dist/apps/shop",
|
||||
"browser": "apps/shop/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
"tsConfig": "apps/shop/tsconfig.app.json",
|
||||
"assets": [
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "apps/shop/public"
|
||||
}
|
||||
],
|
||||
"styles": ["apps/shop/src/styles.css"],
|
||||
"server": "apps/shop/src/main.server.ts",
|
||||
"ssr": {
|
||||
"entry": "apps/shop/src/server.ts"
|
||||
},
|
||||
"outputMode": "server"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "4kb",
|
||||
"maximumError": "8kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
},
|
||||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": true
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"dependsOn": ["api:serve"],
|
||||
"continuous": true,
|
||||
"executor": "@angular/build:dev-server",
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "shop:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "shop:build:development"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development",
|
||||
"options": {
|
||||
"proxyConfig": "apps/shop/proxy.conf.json"
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"executor": "@angular/build:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "shop:build"
|
||||
}
|
||||
},
|
||||
"serve-static": {
|
||||
"continuous": true,
|
||||
"executor": "@nx/web:file-server",
|
||||
"options": {
|
||||
"buildTarget": "shop:build",
|
||||
"port": 4200,
|
||||
"staticFilePath": "dist/apps/shop/browser",
|
||||
"spa": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"/@org/api": {
|
||||
"target": "http://localhost:3333",
|
||||
"secure": false
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,10 +0,0 @@
|
||||
import { mergeApplicationConfig, ApplicationConfig } from '@angular/core';
|
||||
import { provideServerRendering, withRoutes } from '@angular/ssr';
|
||||
import { appConfig } from './app.config';
|
||||
import { serverRoutes } from './app.routes.server';
|
||||
|
||||
const serverConfig: ApplicationConfig = {
|
||||
providers: [provideServerRendering(withRoutes(serverRoutes))],
|
||||
};
|
||||
|
||||
export const config = mergeApplicationConfig(appConfig, serverConfig);
|
||||
@@ -1,22 +0,0 @@
|
||||
import {
|
||||
ApplicationConfig,
|
||||
provideBrowserGlobalErrorListeners,
|
||||
provideZoneChangeDetection,
|
||||
} from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideHttpClient, withFetch } from '@angular/common/http';
|
||||
import { appRoutes } from './app.routes';
|
||||
import {
|
||||
provideClientHydration,
|
||||
withEventReplay,
|
||||
} from '@angular/platform-browser';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideClientHydration(withEventReplay()),
|
||||
provideBrowserGlobalErrorListeners(),
|
||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||
provideRouter(appRoutes),
|
||||
provideHttpClient(withFetch()),
|
||||
],
|
||||
};
|
||||
@@ -1,66 +0,0 @@
|
||||
.app-header {
|
||||
background: #2c3e50;
|
||||
color: white;
|
||||
padding: 1rem 0;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.app-header .container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.app-header h1 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.app-header nav {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.app-header nav a {
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
font-size: 1rem;
|
||||
padding: 8px 16px;
|
||||
border-radius: 4px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.app-header nav a:hover {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.app-header nav a.active {
|
||||
background: #3498db;
|
||||
}
|
||||
|
||||
.app-main {
|
||||
min-height: calc(100vh - 140px);
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.app-footer {
|
||||
background: #34495e;
|
||||
color: white;
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.app-footer .container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.app-footer p {
|
||||
margin: 8px 0;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
<header class="app-header">
|
||||
<div class="container">
|
||||
<h1>{{ title }}</h1>
|
||||
<nav>
|
||||
<a routerLink="/products" routerLinkActive="active">Products</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="app-main">
|
||||
<router-outlet></router-outlet>
|
||||
</main>
|
||||
|
||||
<footer class="app-footer">
|
||||
<div class="container">
|
||||
<p>© 2025 Nx Shop Demo - Showcasing Nx Monorepo Features</p>
|
||||
<p>Frontend (Angular) + Backend (Express) + Shared Libraries</p>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -1,12 +0,0 @@
|
||||
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||
|
||||
export const serverRoutes: ServerRoute[] = [
|
||||
{
|
||||
path: 'products/:id',
|
||||
renderMode: RenderMode.Server,
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
renderMode: RenderMode.Server,
|
||||
},
|
||||
];
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Route } from '@angular/router';
|
||||
|
||||
export const appRoutes: Route[] = [
|
||||
{
|
||||
path: '',
|
||||
redirectTo: 'products',
|
||||
pathMatch: 'full',
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
loadChildren: () =>
|
||||
import('@org/shop/feature-products').then(m => m.featureProductsRoutes),
|
||||
},
|
||||
{
|
||||
path: 'products',
|
||||
loadChildren: () =>
|
||||
import('@org/shop/feature-product-detail').then(
|
||||
m => m.featureProductDetailRoutes
|
||||
),
|
||||
},
|
||||
{
|
||||
path: '**',
|
||||
redirectTo: 'products',
|
||||
},
|
||||
];
|
||||
@@ -1,58 +0,0 @@
|
||||
import { TestBed, ComponentFixture } from '@angular/core/testing';
|
||||
import { App } from './app';
|
||||
import { appRoutes } from './app.routes';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('App', () => {
|
||||
let component: App;
|
||||
let fixture: ComponentFixture<App>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [App],
|
||||
providers: [provideRouter(appRoutes)],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(App);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create the app component', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should render title in header', () => {
|
||||
const compiled = fixture.nativeElement as HTMLElement;
|
||||
expect(compiled.querySelector('h1')?.textContent).toContain('Nx Shop Demo');
|
||||
});
|
||||
|
||||
it('should render navigation links', () => {
|
||||
const navLinks = fixture.debugElement.queryAll(By.css('nav a'));
|
||||
expect(navLinks.length).toBeGreaterThan(0);
|
||||
expect(navLinks[0].nativeElement.textContent).toContain('Products');
|
||||
expect(navLinks[0].nativeElement.getAttribute('routerLink')).toBe(
|
||||
'/products'
|
||||
);
|
||||
});
|
||||
|
||||
it('should render footer with correct copyright', () => {
|
||||
const footer = fixture.nativeElement.querySelector('.app-footer');
|
||||
expect(footer).toBeTruthy();
|
||||
expect(footer?.textContent).toContain('© 2025 Nx Shop Demo');
|
||||
expect(footer?.textContent).toContain(
|
||||
'Frontend (Angular) + Backend (Express) + Shared Libraries'
|
||||
);
|
||||
});
|
||||
|
||||
it('should have router outlet for dynamic content', () => {
|
||||
const routerOutlet = fixture.nativeElement.querySelector('router-outlet');
|
||||
expect(routerOutlet).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should apply change detection strategy OnPush', () => {
|
||||
const metadata = (App as unknown as { ɵcmp: { onPush: boolean } })['ɵcmp'];
|
||||
expect(metadata.onPush).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Component, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
imports: [RouterModule],
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.html',
|
||||
styleUrl: './app.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class App {
|
||||
protected title = 'Nx Shop Demo';
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>shop</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
<body>
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,7 +0,0 @@
|
||||
import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
|
||||
import { App } from './app/app';
|
||||
import { config } from './app/app.config.server';
|
||||
|
||||
const bootstrap = (context: BootstrapContext) => bootstrapApplication(App, config, context);
|
||||
|
||||
export default bootstrap;
|
||||
@@ -1,5 +0,0 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { appConfig } from './app/app.config';
|
||||
import { App } from './app/app';
|
||||
|
||||
bootstrapApplication(App, appConfig).catch((err) => console.error(err));
|
||||
@@ -1,66 +0,0 @@
|
||||
import {
|
||||
AngularNodeAppEngine,
|
||||
createNodeRequestHandler,
|
||||
isMainModule,
|
||||
writeResponseToNodeResponse,
|
||||
} from '@angular/ssr/node';
|
||||
import express from 'express';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
||||
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
||||
|
||||
const app = express();
|
||||
const angularApp = new AngularNodeAppEngine();
|
||||
|
||||
/**
|
||||
* Example Express Rest API endpoints can be defined here.
|
||||
* Uncomment and define endpoints as necessary.
|
||||
*
|
||||
* Example:
|
||||
* ```ts
|
||||
* app.get('/api/**', (req, res) => {
|
||||
* // Handle API request
|
||||
* });
|
||||
* ```
|
||||
*/
|
||||
|
||||
/**
|
||||
* Serve static files from /browser
|
||||
*/
|
||||
app.use(
|
||||
express.static(browserDistFolder, {
|
||||
maxAge: '1y',
|
||||
index: false,
|
||||
redirect: false,
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Handle all other requests by rendering the Angular application.
|
||||
*/
|
||||
app.use('/**', (req, res, next) => {
|
||||
angularApp
|
||||
.handle(req)
|
||||
.then((response) =>
|
||||
response ? writeResponseToNodeResponse(response, res) : next()
|
||||
)
|
||||
.catch(next);
|
||||
});
|
||||
|
||||
/**
|
||||
* Start the server if this module is the main entry point.
|
||||
* The server listens on the port defined by the `PORT` environment variable, or defaults to 4000.
|
||||
*/
|
||||
if (isMainModule(import.meta.url)) {
|
||||
const port = process.env['PORT'] || 4000;
|
||||
app.listen(port, () => {
|
||||
console.log(`Node Express server listening on http://localhost:${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Request handler used by the Angular CLI (for dev-server and during build) or Firebase Cloud Functions.
|
||||
*/
|
||||
export const reqHandler = createNodeRequestHandler(app);
|
||||
@@ -1,38 +0,0 @@
|
||||
/* Global Styles */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen,
|
||||
Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #333;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #3498db;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import '@angular/compiler';
|
||||
import '@analogjs/vitest-angular/setup-zone';
|
||||
|
||||
import {
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting,
|
||||
} from '@angular/platform-browser/testing';
|
||||
import { getTestBed } from '@angular/core/testing';
|
||||
|
||||
getTestBed().initTestEnvironment(
|
||||
BrowserTestingModule,
|
||||
platformBrowserTesting()
|
||||
);
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": [
|
||||
"jest.config.ts",
|
||||
"src/test-setup.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/test-setup.ts"
|
||||
]
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"isolatedModules": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"module": "preserve",
|
||||
"lib": ["dom", "es2022"]
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../dist/out-tsc",
|
||||
"types": [
|
||||
"vitest/globals",
|
||||
"vitest/importMeta",
|
||||
"vite/client",
|
||||
"node",
|
||||
"vitest"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"vite.config.ts",
|
||||
"vite.config.mts",
|
||||
"vitest.config.ts",
|
||||
"vitest.config.mts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.test.tsx",
|
||||
"src/**/*.spec.tsx",
|
||||
"src/**/*.test.js",
|
||||
"src/**/*.spec.js",
|
||||
"src/**/*.test.jsx",
|
||||
"src/**/*.spec.jsx",
|
||||
"src/**/*.d.ts"
|
||||
],
|
||||
"files": ["src/test-setup.ts"]
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
/// <reference types='vitest' />
|
||||
import { defineConfig } from 'vite';
|
||||
import angular from '@analogjs/vite-plugin-angular';
|
||||
import tsconfigPaths from 'vite-tsconfig-paths';
|
||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
||||
|
||||
export default defineConfig(() => ({
|
||||
root: __dirname,
|
||||
cacheDir: '../../node_modules/.vite/apps/shop',
|
||||
plugins: [angular(), tsconfigPaths(), nxCopyAssetsPlugin(['*.md'])],
|
||||
// Uncomment this if you are using workers.
|
||||
// worker: {
|
||||
// plugins: [ nxViteTsPaths() ],
|
||||
// },
|
||||
test: {
|
||||
name: 'shop',
|
||||
watch: false,
|
||||
globals: true,
|
||||
environment: 'jsdom',
|
||||
include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
setupFiles: ['src/test-setup.ts'],
|
||||
reporters: ['default'],
|
||||
coverage: {
|
||||
reportsDirectory: '../../coverage/apps/shop',
|
||||
provider: 'v8' as const,
|
||||
},
|
||||
},
|
||||
}));
|
||||
Reference in New Issue
Block a user