mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-05-25 00:44:03 -04:00
Compare commits
4 Commits
fix/shared
...
feature/in
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
302a76b84a | ||
|
|
2725c6e872 | ||
|
|
7cd2d3e61f | ||
|
|
3c5f4c1465 |
144
.github/workflows/integration-tests.yml
vendored
Normal file
144
.github/workflows/integration-tests.yml
vendored
Normal file
@@ -0,0 +1,144 @@
|
||||
name: Integration Tests
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'public/**'
|
||||
- 'docker/**'
|
||||
- 'docker-compose*.yml'
|
||||
- 'tests/**'
|
||||
- 'integration-tests/**'
|
||||
- '.github/workflows/integration-tests.yml'
|
||||
pull_request:
|
||||
branches: [ master, main ]
|
||||
paths:
|
||||
- 'app/**'
|
||||
- 'public/**'
|
||||
- 'docker/**'
|
||||
- 'docker-compose*.yml'
|
||||
- 'tests/**'
|
||||
- 'integration-tests/**'
|
||||
- '.github/workflows/integration-tests.yml'
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
integration:
|
||||
name: Docker Integration Tests
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Run Basic Integration Tests
|
||||
run: chmod +x integration-tests/run-integration-tests.sh && cd integration-tests && ./run-integration-tests.sh
|
||||
|
||||
- name: View Logs on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Application Logs ==="
|
||||
docker logs opensourcepos-integration-tests-ospos-1
|
||||
echo ""
|
||||
echo "=== Database Logs ==="
|
||||
docker logs mysql || docker logs opensourcepos-mysql || echo "No database logs found"
|
||||
|
||||
- name: Stop Docker Stack
|
||||
if: always()
|
||||
run: docker compose down -v || true
|
||||
|
||||
playwright:
|
||||
name: Playwright E2E Tests
|
||||
runs-on: ubuntu-22.04
|
||||
needs: integration
|
||||
if: always()
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Install Playwright Browsers
|
||||
run: npx playwright install --with-deps chromium firefox
|
||||
|
||||
- name: Start Docker Stack
|
||||
run: docker compose up -d
|
||||
|
||||
- name: Wait for Application
|
||||
run: |
|
||||
echo "Waiting for application to be ready..."
|
||||
timeout 90 bash -c 'until curl -s -f http://localhost/ > /dev/null; do sleep 2; done'
|
||||
echo "Application is ready!"
|
||||
|
||||
- name: Run Playwright Tests
|
||||
run: npm run test
|
||||
env:
|
||||
BASE_URL: http://localhost
|
||||
|
||||
- name: Upload Playwright Report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-report
|
||||
path: integration-tests/playwright-report/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Test Results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-results
|
||||
path: integration-tests/test-results/
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Screenshots on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-screenshots
|
||||
path: integration-tests/test-results/**/*.png
|
||||
retention-days: 7
|
||||
|
||||
- name: Upload Trace Files on Failure
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: playwright-traces
|
||||
path: integration-tests/test-results/**/*.zip
|
||||
retention-days: 7
|
||||
|
||||
- name: View Logs on Failure
|
||||
if: failure()
|
||||
run: |
|
||||
echo "=== Application Logs ==="
|
||||
docker logs opensourcepos-integration-tests-ospos-1
|
||||
echo ""
|
||||
echo "=== Database Logs ==="
|
||||
docker logs mysql || docker logs opensourcepos-mysql || echo "No database logs found"
|
||||
|
||||
- name: Stop Docker Stack
|
||||
if: always()
|
||||
run: docker compose down -v || true
|
||||
137
integration-tests/README.md
Normal file
137
integration-tests/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# Integration Tests for Open Source POS
|
||||
|
||||
This directory contains integration tests for Open Source POS using Docker Compose and Playwright.
|
||||
|
||||
## Test Suites
|
||||
|
||||
### 1. Basic Integration Tests (`run-integration-tests.sh`)
|
||||
Simple HTTP-based tests that verify the application is running and accessible.
|
||||
|
||||
**Tests:**
|
||||
- Application startup
|
||||
- Login page accessibility
|
||||
- HTTP status code validation
|
||||
- Login form presence
|
||||
- Database connectivity (indirect)
|
||||
|
||||
### 2. Playwright E2E Tests (`tests/`)
|
||||
Full browser automation tests using Playwright.
|
||||
|
||||
#### Login Tests (`tests/login.spec.ts`)
|
||||
- Display login page
|
||||
- Login with valid credentials
|
||||
- Reject invalid credentials
|
||||
- Redirect protected pages to login
|
||||
- Console error detection
|
||||
|
||||
#### Item/Inventory Tests (`tests/items.spec.ts`)
|
||||
- Create new item with basic details
|
||||
- Create item with category selection
|
||||
- Update existing item
|
||||
- Verify items appear in inventory table
|
||||
|
||||
#### Customer Tests (`tests/customers.spec.ts`)
|
||||
- Create new customer with basic details
|
||||
- Create customer with complete address information
|
||||
- Search for existing customers
|
||||
- Verify customer details in table format
|
||||
|
||||
#### Sales Tests (`tests/sales.spec.ts`)
|
||||
- Create sale with item and customer
|
||||
- Add payment to sale
|
||||
- Complete sale transaction
|
||||
- Verify receipt generation
|
||||
- Multi-item sale scenarios
|
||||
- Different payment methods (cash)
|
||||
- Receipt validation and display
|
||||
|
||||
#### Combined Operations
|
||||
- Create item and customer sequentially
|
||||
- Verify both entities appear in their respective tables
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Docker and Docker Compose
|
||||
- Node.js 18+
|
||||
- npm
|
||||
|
||||
## Local Setup
|
||||
|
||||
1. Install Node.js dependencies:
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
2. Install Playwright browsers:
|
||||
```bash
|
||||
npx playwright install --with-deps chromium firefox
|
||||
```
|
||||
|
||||
## Running Tests
|
||||
|
||||
### Basic Integration Tests
|
||||
|
||||
```bash
|
||||
chmod +x run-integration-tests.sh
|
||||
./run-integration-tests.sh
|
||||
```
|
||||
|
||||
### Playwright Tests
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Run with UI:
|
||||
```bash
|
||||
npm run test:ui
|
||||
```
|
||||
|
||||
Run with headed browser:
|
||||
```bash
|
||||
npm run test:headed
|
||||
```
|
||||
|
||||
Debug mode:
|
||||
```bash
|
||||
npm run test:debug
|
||||
```
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
The CI pipeline runs both test suites on push/PR to master:
|
||||
|
||||
1. **Integration Job**: Basic Docker stack tests
|
||||
2. **Playwright Job**: Full browser automation tests
|
||||
|
||||
Artifacts uploaded on failure:
|
||||
- Playwright HTML report
|
||||
- Test screenshots
|
||||
- Trace files
|
||||
- Docker container logs
|
||||
|
||||
## Test Results
|
||||
|
||||
- Playwright HTML reports: `playwright-report/`
|
||||
- Test results: `test-results/`
|
||||
- Screenshots (on failure): `test-results/**/*.png`
|
||||
- Traces (on failure): `test-results/**/*.zip`
|
||||
|
||||
## Environment Variables
|
||||
|
||||
- `BASE_URL`: Application base URL (default: http://localhost)
|
||||
|
||||
## Clean Up
|
||||
|
||||
Stop and clean Docker resources:
|
||||
```bash
|
||||
docker compose down -v
|
||||
```
|
||||
|
||||
## Note on Local Playwright Setup
|
||||
|
||||
Playwright requires system dependencies to be installed. If you don't have sudo access, you can:
|
||||
|
||||
1. Use CI environment (GitHub Actions)
|
||||
2. Run Playwright tests in Docker container with proper permissions
|
||||
3. Use the basic integration tests instead
|
||||
32
integration-tests/playwright.config.ts
Normal file
32
integration-tests/playwright.config.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
outputDir: './test-results',
|
||||
fullyParallel: false,
|
||||
forbidOnly: !!process.env.CI,
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
workers: 1,
|
||||
reporter: [
|
||||
['html', { outputFolder: './playwright-report' }],
|
||||
['junit', { outputFile: './test-results/junit.xml' }],
|
||||
['list']
|
||||
],
|
||||
use: {
|
||||
baseURL: 'http://localhost',
|
||||
trace: 'on-first-retry',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
},
|
||||
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
],
|
||||
});
|
||||
96
integration-tests/run-integration-tests.sh
Executable file
96
integration-tests/run-integration-tests.sh
Executable file
@@ -0,0 +1,96 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=== Open Source POS Integration Tests ==="
|
||||
echo ""
|
||||
|
||||
# Start Docker Stack
|
||||
echo "1. Starting Docker Stack..."
|
||||
docker compose up -d
|
||||
|
||||
echo "2. Waiting for application to be ready..."
|
||||
timeout=60
|
||||
elapsed=0
|
||||
while [ $elapsed -lt $timeout ]; do
|
||||
if curl -s -f http://localhost/ > /dev/null 2>&1; then
|
||||
echo " ✓ Application is ready!"
|
||||
break
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
if [ $elapsed -eq $timeout ]; then
|
||||
echo " ✗ Application not ready after ${timeout}s"
|
||||
echo " === Logs ==="
|
||||
docker logs opensourcepos-integration-tests-ospos-1
|
||||
docker logs mysql
|
||||
exit 1
|
||||
fi
|
||||
echo " Waiting... (${elapsed}s)"
|
||||
done
|
||||
|
||||
# Check Login Page
|
||||
echo ""
|
||||
echo "3. Checking Login Page..."
|
||||
response=$(curl -s http://localhost/)
|
||||
|
||||
if echo "$response" | grep -q "Open Source Point of Sale"; then
|
||||
echo " ✓ Login page accessible"
|
||||
else
|
||||
echo " ✗ Login page not accessible"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q "Login"; then
|
||||
echo " ✓ Login form found"
|
||||
else
|
||||
echo " ✗ Login form not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q "username"; then
|
||||
echo " ✓ Username field found"
|
||||
else
|
||||
echo " ✗ Username field not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if echo "$response" | grep -q "password"; then
|
||||
echo " ✓ Password field found"
|
||||
else
|
||||
echo " ✗ Password field not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check HTTP Status
|
||||
echo ""
|
||||
echo "4. Checking HTTP Status..."
|
||||
status=$(curl -s -o /dev/null -w "%{http_code}" http://localhost/)
|
||||
if [ "$status" -eq 200 ]; then
|
||||
echo " ✓ HTTP status: $status"
|
||||
else
|
||||
echo " ✗ HTTP status: $status"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Database Check
|
||||
echo ""
|
||||
echo "5. Checking Database Connection..."
|
||||
db_logs=$(docker logs opensourcepos-integration-tests-ospos-1 2>&1)
|
||||
if echo "$db_logs" | grep -qi "database.*connected\|mysql.*connected\|mysqli.*connected"; then
|
||||
echo " ✓ Database connected"
|
||||
else
|
||||
echo " ⚠ Database connection status unclear (checking if app is responding)"
|
||||
if curl -s -f http://localhost/ > /dev/null; then
|
||||
echo " ✓ Application responding to requests"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "=== All Tests Passed! ✓ ==="
|
||||
|
||||
# Cleanup
|
||||
echo ""
|
||||
echo "6. Stopping Docker Stack..."
|
||||
docker compose down -v
|
||||
|
||||
exit 0
|
||||
273
integration-tests/tests/customers.spec.ts
Normal file
273
integration-tests/tests/customers.spec.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Open Source POS - Customers', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to login page
|
||||
await page.goto('/');
|
||||
|
||||
// Login with admin credentials
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'pointofsale');
|
||||
await page.click('button[type="submit"], input[type="submit"]');
|
||||
|
||||
// Wait for navigation to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check for login errors
|
||||
const errorMessage = page.locator('.alert-danger, .error, .alert[role="alert"]');
|
||||
const errorCount = await errorMessage.count();
|
||||
|
||||
if (errorCount > 0) {
|
||||
const errorText = await errorMessage.first().textContent();
|
||||
console.log('Login error:', errorText);
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
|
||||
test.setTimeout(60000);
|
||||
});
|
||||
|
||||
test('should create a customer and verify it appears in table', async ({ page }) => {
|
||||
// Navigate to customers page
|
||||
const customersLink = page.locator('a[href*="customers"], a:has-text("Customer")');
|
||||
await customersLink.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Look for "Add Customer" or "New Customer" button
|
||||
const addCustomerButton = page.locator('button:has-text("Add"), button:has-text("New"), a:has-text("Add")');
|
||||
await addCustomerButton.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Fill in customer details
|
||||
const firstName = 'John';
|
||||
const lastName = `Test ${Date.now()}`;
|
||||
const email = `test.${Date.now()}@example.com`;
|
||||
const phone = `+1-555-${Math.floor(Math.random() * 9000) + 1000}`;
|
||||
|
||||
await page.fill('input[name="first_name"], input[name="first"], #first_name', firstName);
|
||||
await page.fill('input[name="last_name"], input[name="last"], #last_name', lastName);
|
||||
await page.fill('input[name="email"], #email', email);
|
||||
await page.fill('input[name="phone_number"], input[name="phone"], #phone_number, #phone', phone);
|
||||
|
||||
// Fill address if fields exist
|
||||
const addressField = page.locator('input[name*="address"]').first();
|
||||
const addressCount = await addressField.count();
|
||||
|
||||
if (addressCount > 0) {
|
||||
await addressField.fill('123 Test Street');
|
||||
}
|
||||
|
||||
const cityField = page.locator('input[name="city"], #city');
|
||||
const cityCount = await cityField.count();
|
||||
|
||||
if (cityCount > 0) {
|
||||
await cityField.fill('Test City');
|
||||
}
|
||||
|
||||
// Save the customer
|
||||
const saveButton = page.locator('button[type="submit"], button:has-text("Save"), button:has-text("Submit")');
|
||||
await saveButton.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Navigate back to customers list
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Search for the created customer
|
||||
const searchInput = page.locator('input[name="search"], input[placeholder*="Search"], #search').first();
|
||||
await searchInput.fill(lastName);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify the customer appears in the table
|
||||
const customerName = `${firstName} ${lastName}`;
|
||||
const customerRow = page.locator('table, tbody').locator(`text=${lastName}`).first();
|
||||
await expect(customerRow).toBeVisible({ timeout: 10000 });
|
||||
|
||||
// Also verify email appears if shown in table
|
||||
const emailVisible = await page.locator(`text=${email}`).count();
|
||||
if (emailVisible > 0) {
|
||||
console.log('✓ Customer email also visible in table');
|
||||
}
|
||||
|
||||
console.log('✓ Customer created and verified in table');
|
||||
});
|
||||
|
||||
test('should create a customer with complete details', async ({ page }) => {
|
||||
// Navigate to customers page
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Add new customer
|
||||
await page.getByRole('button', { name: /add|new/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Fill customer details
|
||||
const customerData = {
|
||||
firstName: 'Jane',
|
||||
lastName: `Complete ${Date.now()}`,
|
||||
email: `complete.${Date.now()}@example.com`,
|
||||
phone: `+1-555-${Math.floor(Math.random() * 9000) + 1000}`,
|
||||
address: '456 Complete Ave',
|
||||
city: 'Complete City',
|
||||
state: 'Test State',
|
||||
zip: '12345',
|
||||
country: 'Test Country'
|
||||
};
|
||||
|
||||
await page.fill('input[name="first_name"], input[name="first"]', customerData.firstName);
|
||||
await page.fill('input[name="last_name"], input[name="last"]', customerData.lastName);
|
||||
await page.fill('input[name="email"], #email', customerData.email);
|
||||
await page.fill('input[name="phone_number"], input[name="phone"]', customerData.phone);
|
||||
|
||||
// Fill address fields if they exist
|
||||
await page.fill('input[name^="address"], input[name*="address"]', customerData.address).catch(() => {});
|
||||
await page.fill('input[name="city"], #city', customerData.city).catch(() => {});
|
||||
await page.fill('input[name="state"], #state', customerData.state).catch(() => {});
|
||||
await page.fill('input[name="zip"], input[name="zip_code"], #zip', customerData.zip).catch(() => {});
|
||||
await page.fill('input[name="country"], #country', customerData.country).catch(() => {});
|
||||
|
||||
// Add comments
|
||||
const commentsField = page.locator('textarea[name="comments"], #comments');
|
||||
const commentsCount = await commentsField.count();
|
||||
|
||||
if (commentsCount > 0) {
|
||||
await commentsField.fill('Test customer for automation');
|
||||
}
|
||||
|
||||
// Save customer
|
||||
await page.getByRole('button', { name: /save|submit/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify customer appears in list
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const customerName = `${customerData.firstName} ${customerData.lastName}`;
|
||||
const customerVisible = await page.locator(`text=${lastName}`).count();
|
||||
expect(customerVisible).toBeGreaterThan(0);
|
||||
|
||||
console.log('✓ Customer with complete details created and verified');
|
||||
});
|
||||
|
||||
test('should search for existing customer', async ({ page }) => {
|
||||
// Navigate to customers page
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Search for a customer (John Doe from default database)
|
||||
const searchInput = page.locator('input[name="search"], input[placeholder*="Search"], #search').first();
|
||||
await searchInput.fill('Doe');
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify customer appears
|
||||
const customerVisible = await page.locator('text=Doe').count();
|
||||
expect(customerVisible).toBeGreaterThan(0);
|
||||
|
||||
console.log('✓ Customer search successful');
|
||||
});
|
||||
|
||||
test('should verify customer details in table', async ({ page }) => {
|
||||
// Create a customer first
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.getByRole('button', { name: /add|new/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const customerData = {
|
||||
firstName: 'Table',
|
||||
lastName: `Test ${Date.now()}`,
|
||||
email: `table.${Date.now()}@example.com`,
|
||||
phone: '555-1234'
|
||||
};
|
||||
|
||||
await page.fill('input[name="first_name"], input[name="first"]', customerData.firstName);
|
||||
await page.fill('input[name="last_name"], input[name="last"]', customerData.lastName);
|
||||
await page.fill('input[name="email"], #email', customerData.email);
|
||||
await page.fill('input[name="phone_number"], input[name="phone"]', customerData.phone);
|
||||
|
||||
await page.getByRole('button', { name: /save|submit/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Navigate to customer list
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check table structure
|
||||
const table = page.locator('table').first();
|
||||
await expect(table).toBeVisible();
|
||||
|
||||
// Verify customer data appears
|
||||
const tableContents = await table.textContent();
|
||||
expect(tableContents).toContain(customerData.lastName);
|
||||
|
||||
if (tableContents.includes(customerData.email)) {
|
||||
console.log('✓ Customer email visible in table');
|
||||
}
|
||||
|
||||
if (tableContents.includes(customerData.phone)) {
|
||||
console.log('✓ Customer phone visible in table');
|
||||
}
|
||||
|
||||
console.log('✓ Customer details verified in table format');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Open Source POS - Combined Operations', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'pointofsale');
|
||||
await page.click('button[type="submit"], input[type="submit"]');
|
||||
await page.waitForLoadState('networkidle');
|
||||
test.setTimeout(60000);
|
||||
});
|
||||
|
||||
test('should create item and customer and verify both in their tables', async ({ page }) => {
|
||||
// Create customer first
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.getByRole('button', { name: /add|new/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const customerName = `Combined ${Date.now()}`;
|
||||
await page.fill('input[name="first_name"], input[name="first"]', 'Combined');
|
||||
await page.fill('input[name="last_name"], input[name="last"]', customerName);
|
||||
await page.fill('input[name="email"], #email', `combined.${Date.now()}@example.com`);
|
||||
await page.fill('input[name="phone_number"], input[name="phone"]', '555-9999');
|
||||
|
||||
await page.getByRole('button', { name: /save|submit/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Create item
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.getByRole('button', { name: /add|new/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const itemName = `Combined Item ${Date.now()}`;
|
||||
await page.fill('input[name="name"], input[name="item_name"]', itemName);
|
||||
await page.fill('input[name="item_number"], input[name="number"]', `COMB-${Date.now()}`);
|
||||
await page.fill('input[name="unit_price"], input[name="cost_price"]', '25.00');
|
||||
await page.fill('input[name="quantity"]', '10');
|
||||
|
||||
await page.getByRole('button', { name: /save|submit/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify both exist
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
const customerVisible = await page.locator(`text=${customerName}`).count();
|
||||
expect(customerVisible).toBeGreaterThan(0);
|
||||
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
const itemVisible = await page.locator(`text=${itemName}`).count();
|
||||
expect(itemVisible).toBeGreaterThan(0);
|
||||
|
||||
console.log('✓ Both item and customer created and verified');
|
||||
});
|
||||
});
|
||||
151
integration-tests/tests/items.spec.ts
Normal file
151
integration-tests/tests/items.spec.ts
Normal file
@@ -0,0 +1,151 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Open Source POS - Items', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to login page
|
||||
await page.goto('/');
|
||||
|
||||
// Login with admin credentials
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'pointofsale');
|
||||
await page.click('button[type="submit"], input[type="submit"]');
|
||||
|
||||
// Wait for navigation to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if we're logged in (look for navigation elements)
|
||||
const nav = page.locator('nav, .navbar-nav, .sidebar');
|
||||
|
||||
// If we see error message, login failed
|
||||
const errorMessage = page.locator('.alert-danger, .error, .alert[role="alert"]');
|
||||
const errorCount = await errorMessage.count();
|
||||
|
||||
if (errorCount > 0) {
|
||||
const errorText = await errorMessage.first().textContent();
|
||||
console.log('Login error:', errorText);
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
|
||||
test.setTimeout(60000);
|
||||
});
|
||||
|
||||
test('should create an item and verify it appears in table', async ({ page }) => {
|
||||
// Navigate to items page
|
||||
const navigationOption = page.locator('a[href*="items"], a:has-text("Item"), a:has-text("Inventory")');
|
||||
await navigationOption.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Look for "Add Item" or "New Item" button
|
||||
const addItemButton = page.locator('button:has-text("Add"), button:has-text("New"), a:has-text("Add")');
|
||||
await addItemButton.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Fill in item details
|
||||
const itemName = `Test Item ${Date.now()}`;
|
||||
const itemNumber = `12345-${Date.now()}`;
|
||||
|
||||
await page.fill('input[name="name"], input[name="item_name"], #name, #item_name', itemName);
|
||||
await page.fill('input[name="item_number"], input[name="number"], #item_number', itemNumber);
|
||||
|
||||
// Set price and cost
|
||||
await page.fill('input[name="unit_price"], input[name="cost_price"], #unit_price, #cost_price', '10.00');
|
||||
await page.fill('input[name="cost_price"], #cost_price', '5.00');
|
||||
|
||||
// Set quantity
|
||||
await page.fill('input[name="quantity"], #quantity', '100');
|
||||
|
||||
// Save the item
|
||||
const saveButton = page.locator('button[type="submit"], button:has-text("Save"), button:has-text("Submit")');
|
||||
await saveButton.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Navigate back to items list
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Search for the created item
|
||||
const searchInput = page.locator('input[name="search"], input[placeholder*="Search"], #search').first();
|
||||
await searchInput.fill(itemName);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify the item appears in the table
|
||||
const itemRow = page.locator('table, tbody').locator(`text=${itemName}`).first();
|
||||
await expect(itemRow).toBeVisible({ timeout: 10000 });
|
||||
|
||||
console.log('✓ Item created and verified in table');
|
||||
});
|
||||
|
||||
test('should create an item with category and verify', async ({ page }) => {
|
||||
// Navigate to items page
|
||||
const itemsLink = page.locator('a[href*="items"]');
|
||||
await itemsLink.first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Add new item
|
||||
await page.getByRole('button', { name: /add|new/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Fill item details
|
||||
const itemName = `Category Test Item ${Date.now()}`;
|
||||
await page.fill('input[name="name"], input[name="item_name"]', itemName);
|
||||
await page.fill('input[name="item_number"], input[name="number"]', `CAT-${Date.now()}`);
|
||||
|
||||
// Set price and cost
|
||||
await page.fill('input[name="unit_price"]', '15.99');
|
||||
await page.fill('input[name="cost_price"]', '8.50');
|
||||
|
||||
// Set quantity
|
||||
await page.fill('input[name="quantity"]', '50');
|
||||
|
||||
// Select category (if dropdown exists)
|
||||
const categorySelect = page.locator('select[name*="category"], select[name*="category_id"]');
|
||||
const categoryCount = await categorySelect.count();
|
||||
|
||||
if (categoryCount > 0) {
|
||||
await categorySelect.first().selectOption({ index: 1 }); // Select first available category
|
||||
}
|
||||
|
||||
// Save item
|
||||
await page.getByRole('button', { name: /save|submit/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify item appears in list
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const itemVisible = await page.locator(`text=${itemName}`).count();
|
||||
expect(itemVisible).toBeGreaterThan(0);
|
||||
|
||||
console.log('✓ Item with category created and verified');
|
||||
});
|
||||
|
||||
test('should update an existing item', async ({ page }) => {
|
||||
// Navigate to items page
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Find and click edit button for an item
|
||||
const editButton = page.locator('button:has-text("Edit"), a:has-text("Edit"), .edit').first();
|
||||
await editButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Update item name
|
||||
const updatedName = `Updated Item ${Date.now()}`;
|
||||
const nameInput = page.locator('input[name="name"], input[name="item_name"]');
|
||||
await nameInput.fill(updatedName);
|
||||
|
||||
// Save changes
|
||||
await page.getByRole('button', { name: /save|submit|update/i }).first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Navigate back and verify update
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const updatedItemVisible = await page.locator(`text=${updatedName}`).count();
|
||||
expect(updatedItemVisible).toBeGreaterThan(0);
|
||||
|
||||
console.log('✓ Item updated successfully');
|
||||
});
|
||||
});
|
||||
98
integration-tests/tests/login.spec.ts
Normal file
98
integration-tests/tests/login.spec.ts
Normal file
@@ -0,0 +1,98 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Open Source POS - Login', () => {
|
||||
let baseUrl: string;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
baseUrl = process.env.BASE_URL || 'http://localhost';
|
||||
test.setTimeout(60000);
|
||||
});
|
||||
|
||||
test('should display login page', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Check page title
|
||||
await expect(page).toHaveTitle(/Open Source Point of Sale/);
|
||||
|
||||
// Check for login form
|
||||
await expect(page.locator('input[name="username"]')).toBeVisible();
|
||||
await expect(page.locator('input[name="password"]')).toBeVisible();
|
||||
|
||||
// Check for login button
|
||||
const submitButton = page.locator('button[type="submit"], input[type="submit"]');
|
||||
await expect(submitButton).toBeVisible();
|
||||
});
|
||||
|
||||
test('should login with valid credentials', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Enter credentials
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'pointofsale');
|
||||
|
||||
// Submit login form
|
||||
await page.click('button[type="submit"], input[type="submit"]');
|
||||
|
||||
// Wait for navigation
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if redirected to dashboard (login successful if no error message)
|
||||
const errorMessage = page.locator('.alert-danger, .error, .alert[role="alert"]');
|
||||
const errorCount = await errorMessage.count();
|
||||
|
||||
if (errorCount === 0) {
|
||||
// Login successful - check for dashboard elements
|
||||
const dashboardElements = page.locator('nav, .navbar, [role="navigation"]');
|
||||
await expect(dashboardElements.first()).toBeVisible({ timeout: 10000 });
|
||||
} else {
|
||||
// Failed login - check error message
|
||||
const errorText = await errorMessage.first().textContent();
|
||||
console.log('Login error:', errorText);
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('should reject invalid credentials', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
|
||||
// Enter wrong credentials
|
||||
await page.fill('input[name="username"]', 'invalid');
|
||||
await page.fill('input[name="password"]', 'wrongpassword');
|
||||
|
||||
// Submit login form
|
||||
await page.click('button[type="submit"], input[type="submit"]');
|
||||
|
||||
// Wait for response
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check for error message
|
||||
const errorMessage = page.locator('.alert-danger, .error, .alert[role="alert"]');
|
||||
await expect(errorMessage.first()).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should redirect to login when accessing protected page', async ({ page }) => {
|
||||
// Try to access a protected route directly
|
||||
await page.goto('/home');
|
||||
|
||||
// Should redirect to login
|
||||
await expect(page.locator('input[name="username"]')).toBeVisible({ timeout: 10000 });
|
||||
});
|
||||
|
||||
test('should have no console errors', async ({ page }) => {
|
||||
const errors: string[] = [];
|
||||
|
||||
page.on('console', msg => {
|
||||
if (msg.type() === 'error') {
|
||||
errors.push(msg.text());
|
||||
}
|
||||
});
|
||||
|
||||
await page.goto('/');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
if (errors.length > 0) {
|
||||
console.error('Console errors found:', errors);
|
||||
throw new Error(`Found ${errors.length} console errors`);
|
||||
}
|
||||
});
|
||||
});
|
||||
355
integration-tests/tests/sales.spec.ts
Normal file
355
integration-tests/tests/sales.spec.ts
Normal file
@@ -0,0 +1,355 @@
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Open Source POS - Sales', () => {
|
||||
let itemName: string;
|
||||
let customerName: string;
|
||||
let itemNumber: string;
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
// Navigate to login page
|
||||
await page.goto('/');
|
||||
|
||||
// Login with admin credentials
|
||||
await page.fill('input[name="username"]', 'admin');
|
||||
await page.fill('input[name="password"]', 'pointofsale');
|
||||
await page.click('button[type="submit"], input[type="submit"]');
|
||||
|
||||
// Wait for navigation to complete
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check for login errors
|
||||
const errorMessage = page.locator('.alert-danger, .error, .alert[role="alert"]');
|
||||
const errorCount = await errorMessage.count();
|
||||
|
||||
if (errorCount > 0) {
|
||||
const errorText = await errorMessage.first().textContent();
|
||||
console.log('Login error:', errorText);
|
||||
throw new Error(`Login failed: ${errorText}`);
|
||||
}
|
||||
|
||||
test.setTimeout(120000);
|
||||
});
|
||||
|
||||
test('should create sale with item and customer, add payment and complete', async ({ page }) => {
|
||||
console.log('Step 1: Creating test item...');
|
||||
// Create a test item first
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addButton = page.locator('button:has-text("Add"), button:has-text("New"), a:has-text("Add")').first();
|
||||
await addButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
itemName = `Sale Test Item ${Date.now()}`;
|
||||
itemNumber = `SALE-${Date.now()}`;
|
||||
|
||||
await page.fill('input[name="name"], input[name="item_name"], #name, #item_name', itemName);
|
||||
await page.fill('input[name="item_number"], input[name="number"], #item_number', itemNumber);
|
||||
await page.fill('input[name="unit_price"], input[name="cost_price"], #unit_price, #cost_price', '25.00');
|
||||
await page.fill('input[name="cost_price"], #cost_price', '10.00');
|
||||
await page.fill('input[name="quantity"], #quantity', '100');
|
||||
|
||||
const saveButton = page.locator('button[type="submit"], button:has-text("Save"), button:has-text("Submit")').first();
|
||||
await saveButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('✓ Test item created');
|
||||
|
||||
console.log('Step 2: Creating test customer...');
|
||||
// Create a test customer
|
||||
await page.goto('/customers');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const addCustomerButton = page.locator('button:has-text("Add"), button:has-text("New"), a:has-text("Add")').first();
|
||||
await addCustomerButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const firstName = 'Sale';
|
||||
const lastName = `Test ${Date.now()}`;
|
||||
customerName = `${firstName} ${lastName}`;
|
||||
|
||||
await page.fill('input[name="first_name"], input[name="first"], #first_name', firstName);
|
||||
await page.fill('input[name="last_name"], input[name="last"], #last_name', lastName);
|
||||
await page.fill('input[name="email"], #email', `sale.test.${Date.now()}@example.com`);
|
||||
await page.fill('input[name="phone_number"], input[name="phone"], #phone_number, #phone', '555-7777');
|
||||
|
||||
const saveCustomerButton = page.locator('button[type="submit"], button:has-text("Save"), button:has-text("Submit")').first();
|
||||
await saveCustomerButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
console.log('✓ Test customer created');
|
||||
|
||||
console.log('Step 3: Starting new sale...');
|
||||
// Navigate to sales page
|
||||
const salesLink = page.locator('a[href*="sales"], a:has-text("Sale"), a:has-text("POS")').first();
|
||||
await salesLink.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Start new sale (if needed)
|
||||
const newSaleButton = page.locator('button:has-text("New"), button:has-text("New Sale"), a:has-text("New")').first();
|
||||
const newSaleCount = await newSaleButton.count();
|
||||
|
||||
if (newSaleCount > 0) {
|
||||
await newSaleButton.click();
|
||||
await page.waitForLoadState('networkidle', { timeout: 5000 });
|
||||
}
|
||||
|
||||
console.log('✓ New sale started');
|
||||
|
||||
console.log('Step 4: Adding item to cart...');
|
||||
// Add item to cart
|
||||
// Look for item search or add field
|
||||
const itemSearchInput = page.locator('input[name="item"], input[placeholder*="Item"], input[placeholder*="Search"], #item_search, .item-search').first();
|
||||
await itemSearchInput.fill(itemName);
|
||||
|
||||
// Press Enter or click add button
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Alternative: look for add button next to item field
|
||||
const addToCartButton = page.locator('button:has-text("Add"), button:has-text("Add Item"), .add-item').first();
|
||||
const addToCartCount = await addToCartButton.count();
|
||||
|
||||
if (addToCartCount > 0) {
|
||||
await addToCartButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
console.log('✓ Item added to cart');
|
||||
|
||||
console.log('Step 5: Adding customer to sale...');
|
||||
// Add customer to sale
|
||||
const customerSelect = page.locator('select[name*="customer"], select[name*="customer_id"], input[name*="customer"], .customer-select').first();
|
||||
const customerSelectCount = await customerSelect.count();
|
||||
|
||||
if (customerSelectCount > 0) {
|
||||
const tagName = await customerSelect.first().evaluate(el => el.tagName.toLowerCase());
|
||||
|
||||
if (tagName === 'select') {
|
||||
await customerSelect.selectOption({ label: new RegExp(lastName) }).catch(() => {
|
||||
// If select by label doesn't work, try by value
|
||||
return customerSelect.selectOption({ index: 1 });
|
||||
});
|
||||
} else {
|
||||
await customerSelect.fill(lastName);
|
||||
await page.keyboard.press('Enter');
|
||||
}
|
||||
}
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
console.log('✓ Customer added to sale');
|
||||
|
||||
console.log('Step 6: Verifying cart contents...');
|
||||
// Verify item is in cart
|
||||
const cartTable = page.locator('table.cart, table tbody, .cart-items, .sales-table');
|
||||
const cartVisible = await cartTable.count();
|
||||
|
||||
if (cartVisible > 0) {
|
||||
const cartContents = await cartTable.first().textContent();
|
||||
expect(cartContents).toContain(itemName || itemNumber);
|
||||
console.log('✓ Item verified in cart');
|
||||
}
|
||||
|
||||
console.log('Step 7: Adding payment...');
|
||||
// Add payment
|
||||
const paymentButton = page.locator('button:has-text("Payment"), button:has-text("Pay"), button:has-text("Checkout")').first();
|
||||
await paymentButton.click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Select payment method
|
||||
const paymentMethodSelect = page.locator('select[name*="payment_method"], select[name*="payment"]').first();
|
||||
const paymentMethodCount = await paymentMethodSelect.count();
|
||||
|
||||
if (paymentMethodCount > 0) {
|
||||
await paymentMethodSelect.selectOption('Cash').catch(() => {
|
||||
return paymentMethodSelect.selectOption({ index: 0 });
|
||||
});
|
||||
}
|
||||
|
||||
// Enter payment amount (should auto-fill with total)
|
||||
const paymentAmountInput = page.locator('input[name*="payment_amount"], input[name*="amount"], .payment-amount').first();
|
||||
const paymentAmountCount = await paymentAmountInput.count();
|
||||
|
||||
if (paymentAmountCount > 0) {
|
||||
const currentValue = await paymentAmountInput.inputValue();
|
||||
if (!currentValue || parseFloat(currentValue) === 0) {
|
||||
await paymentAmountInput.fill('25.00');
|
||||
}
|
||||
}
|
||||
|
||||
console.log('✓ Payment added');
|
||||
|
||||
console.log('Step 8: Completing sale...');
|
||||
// Complete/Confirm sale
|
||||
const confirmButton = page.locator('button:has-text("Complete"), button:has-text("Confirm"), button:has-text("Submit"), button:has-text("Finish")').first();
|
||||
await confirmButton.click();
|
||||
await page.waitForLoadState('networkidle', { timeout: 15000 });
|
||||
|
||||
console.log('✓ Sale completed');
|
||||
|
||||
console.log('Step 9: Checking for receipt...');
|
||||
// Check if receipt is generated
|
||||
// Look for receipt modal, popup, or new page
|
||||
const receiptModal = page.locator('.receipt, .modal, dialog, #receipt, [class*="receipt"]').first();
|
||||
const receiptModalCount = await receiptModal.count();
|
||||
|
||||
if (receiptModalCount > 0) {
|
||||
await expect(receiptModal.first()).toBeVisible({ timeout: 10000 });
|
||||
console.log('✓ Receipt modal displayed');
|
||||
|
||||
// Verify receipt contains sale details
|
||||
const receiptText = await receiptModal.first().textContent();
|
||||
expect(receiptText).toBeTruthy();
|
||||
|
||||
if (itemName) {
|
||||
expect(receiptText.toLowerCase()).toContain(itemName.toLowerCase()).catch(() => {
|
||||
console.log('Note: Item name not found in receipt, but receipt is visible');
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Check for receipt in page content
|
||||
const receiptContent = await page.textContent();
|
||||
expect(receiptContent).toMatch(/receipt|sale|total/i);
|
||||
console.log('✓ Receipt content found on page');
|
||||
}
|
||||
|
||||
console.log('Step 10: Verifying sale details...');
|
||||
// Verify success message
|
||||
const successMessage = page.locator('.alert-success, .success, .success-message, .flash-success').first();
|
||||
const successCount = await successMessage.count();
|
||||
|
||||
if (successCount > 0) {
|
||||
await expect(successMessage.first()).toBeVisible({ timeout: 10000 });
|
||||
console.log('✓ Success message displayed');
|
||||
}
|
||||
|
||||
// Close receipt/modal if there's a close button
|
||||
const closeButton = page.locator('button:has-text("Close"), button:has-text("OK"), .close, .modal-close, [aria-label="Close"]').first();
|
||||
const closeCount = await closeButton.count();
|
||||
|
||||
if (closeCount > 0) {
|
||||
await closeButton.click().catch(() => {
|
||||
console.log('Close button click failed, but that might be OK');
|
||||
});
|
||||
}
|
||||
|
||||
console.log('\n=== Sale flow completed successfully! ===');
|
||||
});
|
||||
|
||||
test('should create sale with multiple items', async ({ page }) => {
|
||||
console.log('Creating test items for multi-item sale...');
|
||||
|
||||
// Create two test items
|
||||
const items = [];
|
||||
for (let i = 1; i <= 2; i++) {
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.locator('button:has-text("Add"), button:has-text("New")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const itemData = {
|
||||
name: `Multi Item ${i} ${Date.now()}`,
|
||||
number: `MUL-${Date.now()}-${i}`,
|
||||
price: (10 * i).toFixed(2),
|
||||
cost: (5 * i).toFixed(2),
|
||||
quantity: '50'
|
||||
};
|
||||
|
||||
await page.fill('input[name="name"], input[name="item_name"], #name', itemData.name);
|
||||
await page.fill('input[name="item_number"], input[name="number"], #item_number', itemData.number);
|
||||
await page.fill('input[name="unit_price"], #unit_price', itemData.price);
|
||||
await page.fill('input[name="cost_price"], #cost_price', itemData.cost);
|
||||
await page.fill('input[name="quantity"], #quantity', itemData.quantity);
|
||||
|
||||
await page.locator('button[type="submit"], button:has-text("Save")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
items.push(itemData);
|
||||
}
|
||||
|
||||
console.log('✓ Test items created');
|
||||
|
||||
console.log('Starting multi-item sale...');
|
||||
// Navigate to sales
|
||||
await page.goto('/sales');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Add items to cart
|
||||
for (const item of items) {
|
||||
const itemSearch = page.locator('input[name="item"], input[placeholder*="Item"], .item-search').first();
|
||||
await itemSearch.fill(item.name);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
console.log('✓ Items added to cart');
|
||||
|
||||
// Complete payment and sale
|
||||
await page.locator('button:has-text("Payment"), button:has-text("Pay")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
await page.locator('button:has-text("Complete"), button:has-text("Confirm")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify receipt
|
||||
const receiptModal = page.locator('.receipt, .modal, #receipt').first();
|
||||
await expect(receiptModal.first()).toBeVisible({ timeout: 10000 });
|
||||
|
||||
console.log('✓ Multi-item sale completed with receipt');
|
||||
});
|
||||
|
||||
test('should complete sale with cash payment', async ({ page }) => {
|
||||
console.log('Testing cash payment flow...');
|
||||
|
||||
// Use existing item (if available) or create one
|
||||
await page.goto('/items');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Check if there are existing items
|
||||
const itemRows = page.locator('table tbody tr').count();
|
||||
let itemToUse = 'Test Item';
|
||||
|
||||
if (itemRows === 0) {
|
||||
await page.locator('button:has-text("Add"), button:has-text("New")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
itemToUse = `Cash Test ${Date.now()}`;
|
||||
await page.fill('input[name="name"], input[name="item_name"], #name', itemToUse);
|
||||
await page.fill('input[name="item_number"], input[name="number"], #item_number', `CASH-${Date.now()}`);
|
||||
await page.fill('input[name="unit_price"], #unit_price', '50.00');
|
||||
await page.fill('input[name="cost_price"], #cost_price', '25.00');
|
||||
await page.fill('input[name="quantity"], #quantity', '20');
|
||||
|
||||
await page.locator('button:has-text("Save")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
}
|
||||
|
||||
console.log('Creating sale with cash payment...');
|
||||
await page.goto('/sales');
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Add item
|
||||
const itemSearch = page.locator('input[name="item"], input[placeholder*="Item"], .item-search').first();
|
||||
await itemSearch.fill(itemToUse);
|
||||
await page.keyboard.press('Enter');
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// Select cash payment
|
||||
await page.locator('button:has-text("Payment"), button:has-text("Pay")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
const paymentMethod = page.locator('select[name*="payment_method"], select[name*="payment"]').first();
|
||||
await paymentMethod.selectOption('Cash').catch(() => paymentMethod.selectOption({ index: 0 }));
|
||||
|
||||
// Complete sale
|
||||
await page.locator('button:has-text("Complete"), button:has-text("Confirm")').first().click();
|
||||
await page.waitForLoadState('networkidle');
|
||||
|
||||
// Verify receipt is generated
|
||||
const receiptVisible = await page.locator('.receipt, .modal, #receipt').count();
|
||||
expect(receiptVisible).toBeGreaterThan(0);
|
||||
|
||||
console.log('✓ Cash payment sale completed with receipt');
|
||||
});
|
||||
});
|
||||
65
package-lock.json
generated
65
package-lock.json
generated
@@ -43,6 +43,8 @@
|
||||
"tableexport.jquery.plugin": "^1.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@types/node": "^20.11.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-clean": "^0.4.0",
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
@@ -100,6 +102,22 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@playwright/test": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz",
|
||||
"integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@popperjs/core": {
|
||||
"version": "2.11.8",
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
@@ -4448,6 +4466,53 @@
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz",
|
||||
"integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"playwright-core": "1.58.2"
|
||||
},
|
||||
"bin": {
|
||||
"playwright": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright-core": {
|
||||
"version": "1.58.2",
|
||||
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz",
|
||||
"integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"playwright-core": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/playwright/node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/plugin-error": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/plugin-error/-/plugin-error-0.1.2.tgz",
|
||||
|
||||
@@ -26,7 +26,12 @@
|
||||
},
|
||||
"scripts": {
|
||||
"build": "gulp default",
|
||||
"gulp": "gulp"
|
||||
"gulp": "gulp",
|
||||
"test": "cd integration-tests && playwright test",
|
||||
"test:headed": "cd integration-tests && playwright test --headed",
|
||||
"test:ui": "cd integration-tests && playwright test --ui",
|
||||
"test:debug": "cd integration-tests && playwright test --debug",
|
||||
"test:install": "cd integration-tests && playwright install --with-deps"
|
||||
},
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
@@ -64,6 +69,8 @@
|
||||
"tableexport.jquery.plugin": "^1.30.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.44.0",
|
||||
"@types/node": "^20.11.0",
|
||||
"gulp": "^5.0.0",
|
||||
"gulp-clean": "^0.4.0",
|
||||
"gulp-clean-css": "^4.3.0",
|
||||
|
||||
Reference in New Issue
Block a user