Compare commits

..

1 Commits

Author SHA1 Message Date
32b69b504f Build [v1.10.17] 2026-04-19 08:04:18 +03:00
84 changed files with 464 additions and 6963 deletions

View File

@@ -46,25 +46,15 @@
"mcp__plugin_context-mode_context-mode__ctx_stats",
"mcp__plugin_context-mode_context-mode__ctx_execute_file",
"Bash(git checkout *)",
"Bash(git stash *)",
"Bash(python -m pytest backend/tests/ -v --tb=short)",
"Bash(sed -i 's|\"/api/users|\"/users|g' backend/tests/test_users.py)",
"Bash(sed -i 's|\"/api/items|\"/items|g' backend/tests/test_items.py)",
"Bash(sed -i 's|\"/api/categories|\"/categories|g' backend/tests/test_categories.py)",
"Bash(sed -i 's|\"/api/operations|\"/operations|g' backend/tests/test_operations.py)",
"Bash(sed -i 's|\"/api/operations|\"/operations|g' backend/tests/test_offline_sync.py)",
"Bash(curl -sf http://localhost:8906/)",
"Bash(curl -sf http://localhost:3000/)",
"Bash(curl -s -o /dev/null -w \"%{http_code}\" http://localhost:3000/)",
"Bash(NEXT_PUBLIC_API_URL=http://localhost:8906 npm run dev -- --port 3000)",
"Bash(echo \"Frontend PID: $!\")",
"Bash(curl -sf http://localhost:3000)",
"Bash(curl -s http://localhost:3000/login)",
"Bash(curl -s http://localhost:8906/users)",
"Bash(curl -v http://localhost:8906/users)",
"Bash(NEXT_PUBLIC_API_URL=http://localhost:8916 npm run dev -- --port 8917)",
"Bash(curl -sf http://localhost:8917)",
"Bash(xargs sed *)"
"Bash(python -m py_compile backend/tests/conftest.py)",
"Bash(python -m py_compile backend/tests/test_users.py)",
"Bash(python *)",
"Bash(python3.12 -m venv backend/venv)",
"Bash(backend/venv/bin/pip install *)",
"Bash(sudo apt-get *)",
"Bash(PYTHONPATH=/data/programare_AI/tfm_ainventory:/data/programare_AI/tfm_ainventory/backend backend/venv/bin/pytest backend/tests/test_items.py::TestItemCRUD::test_create_item -v)",
"Bash(PYTHONPATH=/data/programare_AI/tfm_ainventory:/data/programare_AI/tfm_ainventory/backend backend/venv/bin/pytest backend/tests/test_items.py::TestItemCRUD::test_create_item -vv)",
"Bash(git tag *)"
]
}
}

View File

@@ -13,8 +13,6 @@ from backend.database import Base, get_db
from backend.models import User
from backend.config_manager import ConfigManager
from backend.auth import get_current_admin, TokenData
import backend.routers.users as users_router
import backend.routers.categories as categories_router
# Test data constants
@@ -71,10 +69,7 @@ def test_client(test_db: Session) -> Generator[TestClient, None, None]:
finally:
pass
# Override all local get_db functions across all routers
app.dependency_overrides[get_db] = override_get_db
app.dependency_overrides[users_router.get_db] = override_get_db
app.dependency_overrides[categories_router.get_db] = override_get_db
client = TestClient(app)
yield client
@@ -84,19 +79,16 @@ def test_client(test_db: Session) -> Generator[TestClient, None, None]:
@pytest.fixture(scope="function")
def mock_ldap() -> Generator[MagicMock, None, None]:
"""Mock LDAP authentication."""
with patch("backend.routers.users.ldap3.Server") as mock_server, \
patch("backend.routers.users.ldap3.Connection") as mock_conn_class:
with patch("backend.auth.ldap3.Server") as mock_server, \
patch("backend.auth.ldap3.Connection") as mock_conn_class:
mock_conn = MagicMock()
mock_conn.bind.return_value = True
mock_conn.search.return_value = True
mock_entry = MagicMock()
mock_entry.entry_dn = TEST_LDAP_DN
mock_entry.uid = MagicMock()
mock_entry.uid.values = ["testuser"]
mock_entry.memberOf = MagicMock()
mock_entry.memberOf.values = ["cn=inventory_users,ou=groups,dc=ainventory,dc=local"]
mock_conn.entries = [mock_entry]
mock_conn.entries = [
MagicMock(entry_dn=TEST_LDAP_DN,
uid=["testuser"])
]
mock_conn_class.return_value = mock_conn
yield mock_conn
@@ -104,17 +96,23 @@ def mock_ldap() -> Generator[MagicMock, None, None]:
@pytest.fixture(scope="function")
def mock_gemini() -> Generator[MagicMock, None, None]:
"""Mock AI label extraction at the ai_vision level."""
with patch("backend.ai_vision.extract_label_info") as mock_gen:
mock_gen.return_value = TEST_AI_RESPONSE
"""Mock Google Gemini AI extraction."""
with patch("backend.ai.gemini_extractor.generate_content") as mock_gen:
mock_response = MagicMock()
mock_response.text = json.dumps(TEST_AI_RESPONSE)
mock_gen.return_value = mock_response
yield mock_gen
@pytest.fixture(scope="function")
def mock_claude() -> Generator[MagicMock, None, None]:
"""Mock AI label extraction at the ai_vision level (Claude)."""
with patch("backend.ai_vision.extract_label_info") as mock_gen:
mock_gen.return_value = TEST_AI_RESPONSE
"""Mock Anthropic Claude AI extraction."""
with patch("backend.ai.claude_extractor.generate_content") as mock_gen:
mock_content = MagicMock()
mock_content.text = json.dumps(TEST_AI_RESPONSE)
mock_response = MagicMock()
mock_response.content = [mock_content]
mock_gen.return_value = mock_response
yield mock_gen

View File

@@ -1,46 +1,35 @@
import pytest
from fastapi import status
def test_get_backups(test_client, admin_token):
response = test_client.get(
"/admin/db/backups",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
def test_get_backups(client):
response = client.get("/admin/db/backups")
assert response.status_code == 200
assert isinstance(response.json(), list)
def test_db_settings_workflow(test_client, admin_token):
def test_db_settings_workflow(client):
# GET settings
response = test_client.get(
"/admin/db/settings",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
response = client.get("/admin/db/settings")
assert response.status_code == 200
data = response.json()
assert "retention_count" in data
# PATCH settings
new_settings = {
"retention_count": 25,
"schedule_hour": 5,
"schedule_freq_days": 2
}
response = test_client.patch(
"/admin/db/settings",
json=new_settings,
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
response = client.patch("/admin/db/settings", json=new_settings)
assert response.status_code == 200
assert response.json()["retention_count"] == 25
# Verify persistence
response = client.get("/admin/db/settings")
assert response.json()["retention_count"] == 25
def test_ai_config(test_client, admin_token):
response = test_client.get(
"/admin/db/settings/ai",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
def test_ai_config(client):
response = client.get("/admin/db/settings/ai")
assert response.status_code == 200
data = response.json()
assert "active_provider" in data
assert "providers" in data
assert len(data["providers"]) == 2

View File

@@ -1,62 +1,67 @@
import pytest
import io
from fastapi import status
from unittest.mock import patch
# Minimal valid 1x1 PNG bytes
MINIMAL_PNG = (
b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01'
b'\x00\x00\x00\x01\x08\x02\x00\x00\x00\x90wS\xde\x00\x00'
b'\x00\x0cIDATx\x9cc\xf8\x0f\x00\x00\x01\x01\x00\x05\x18'
b'\xd8N\x00\x00\x00\x00IEND\xaeB`\x82'
)
class TestAIExtraction:
"""Test AI label extraction pipeline (mocked)."""
def test_gemini_extraction(self, test_client, user_token, mock_gemini):
def test_gemini_extraction(self, test_client, mock_gemini):
"""Test AI extraction using Gemini."""
response = test_client.post(
"/items/extract-label?mode=item",
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
headers={"Authorization": f"Bearer {user_token}"}
"/api/ai/extract",
json={
"image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"provider": "gemini"
}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "name" in data
assert data["name"] == "Test Item"
assert data["part_number"] == "PN-12345"
def test_claude_extraction(self, test_client, user_token, mock_claude):
def test_claude_extraction(self, test_client, mock_claude):
"""Test AI extraction using Claude."""
response = test_client.post(
"/items/extract-label?mode=item",
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
headers={"Authorization": f"Bearer {user_token}"}
"/api/ai/extract",
json={
"image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"provider": "claude"
}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["name"] == "Test Item"
def test_extraction_box_mode(self, test_client, user_token, mock_gemini):
def test_extraction_box_mode(self, test_client, mock_gemini):
"""Test AI extraction in 'box' mode (focus on labels)."""
response = test_client.post(
"/items/extract-label?mode=box",
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
headers={"Authorization": f"Bearer {user_token}"}
"/api/ai/extract",
json={
"image_base64": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==",
"provider": "gemini",
"mode": "box"
}
)
assert response.status_code == status.HTTP_200_OK
def test_extraction_invalid_file_type(self, test_client, user_token):
"""Test that invalid file type is rejected."""
def test_extraction_invalid_provider(self, test_client):
"""Test that invalid provider fails."""
response = test_client.post(
"/items/extract-label",
files={"file": ("test.txt", io.BytesIO(b"not an image"), "text/plain")},
headers={"Authorization": f"Bearer {user_token}"}
"/api/ai/extract",
json={
"image_base64": "invalid",
"provider": "invalid_provider"
}
)
assert response.status_code == status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_extraction_missing_file(self, test_client, user_token):
"""Test that missing file returns 422."""
def test_extraction_missing_image(self, test_client):
"""Test that missing image fails."""
response = test_client.post(
"/items/extract-label",
headers={"Authorization": f"Bearer {user_token}"}
"/api/ai/extract",
json={"provider": "gemini"}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
@@ -64,27 +69,17 @@ class TestAIExtraction:
class TestAIValidation:
"""Test AI extraction validation (user confirmation before save)."""
def test_extraction_requires_auth(self, test_client):
"""Test that extraction endpoint requires authentication."""
def test_validate_extraction(self, test_client, test_db):
"""Test that extracted data requires user validation."""
response = test_client.post(
"/items/extract-label",
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")}
)
assert response.status_code in (status.HTTP_401_UNAUTHORIZED, status.HTTP_403_FORBIDDEN)
def test_extraction_result_not_auto_saved(self, test_client, test_db, user_token, mock_gemini):
"""Test that extracted data is returned but not auto-saved to DB."""
from backend.models import Item
initial_count = test_db.query(Item).count()
response = test_client.post(
"/items/extract-label?mode=item",
files={"file": ("test.png", io.BytesIO(MINIMAL_PNG), "image/png")},
headers={"Authorization": f"Bearer {user_token}"}
"/api/ai/extract",
json={
"image_base64": "xyz",
"provider": "gemini",
"save": False
}
)
assert response.status_code == status.HTTP_200_OK
# Item count should be unchanged — extraction never auto-saves
final_count = test_db.query(Item).count()
assert initial_count == final_count
data = response.json()
assert "extracted_data" in data
assert "user_confirmation_required" in data or data.get("save") == False

View File

@@ -5,18 +5,33 @@ from fastapi import status
class TestCategoryCRUD:
"""Test category creation, read, update, delete."""
def test_create_category(self, test_client, user_token):
def test_create_category(self, test_client):
"""Test creating a category."""
response = test_client.post(
"/categories",
json={"name": "Electronics", "description": "Electronic components"},
headers={"Authorization": f"Bearer {user_token}"}
"/api/categories",
json={
"name": "Electronics",
"description": "Electronic components"
}
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["name"] == "Electronics"
def test_get_category_by_id(self, test_client, test_db):
"""Test retrieving a category by ID."""
from backend.models import Category
category = Category(name="Electronics", description="Electronic components")
test_db.add(category)
test_db.commit()
response = test_client.get(f"/api/categories/{category.id}")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["name"] == "Electronics"
def test_list_categories(self, test_client, test_db, user_token):
def test_list_categories(self, test_client, test_db):
"""Test listing all categories."""
from backend.models import Category
@@ -25,15 +40,12 @@ class TestCategoryCRUD:
test_db.add_all([cat1, cat2])
test_db.commit()
response = test_client.get(
"/categories",
headers={"Authorization": f"Bearer {user_token}"}
)
response = test_client.get("/api/categories")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data) >= 2
def test_update_category(self, test_client, test_db, user_token):
def test_update_category(self, test_client, test_db):
"""Test updating a category."""
from backend.models import Category
@@ -42,40 +54,28 @@ class TestCategoryCRUD:
test_db.commit()
response = test_client.put(
f"/categories/{category.id}",
json={"name": "Electronics", "description": "New description"},
headers={"Authorization": f"Bearer {user_token}"}
f"/api/categories/{category.id}",
json={"description": "New description"}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["description"] == "New description"
def test_delete_category_admin_only(self, test_client, test_db, admin_token, user_token):
def test_delete_category_admin_only(self, test_client, test_db, admin_token):
"""Test deleting a category (admin only)."""
from backend.models import Category
category = Category(name="ToDelete", description="Desc")
category = Category(name="Electronics", description="Desc")
test_db.add(category)
test_db.commit()
cat_id = category.id
response = test_client.delete(
f"/categories/{cat_id}",
f"/api/categories/{cat_id}",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
assert response.status_code == status.HTTP_204_NO_CONTENT
def test_create_duplicate_category_fails(self, test_client, test_db, user_token):
"""Test that duplicate category names are rejected."""
from backend.models import Category
cat = Category(name="Duplicate", description="Existing")
test_db.add(cat)
test_db.commit()
response = test_client.post(
"/categories",
json={"name": "Duplicate", "description": "Another"},
headers={"Authorization": f"Bearer {user_token}"}
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
# Verify deletion
response = test_client.get(f"/api/categories/{cat_id}")
assert response.status_code == status.HTTP_404_NOT_FOUND

View File

@@ -5,42 +5,40 @@ from fastapi import status
class TestItemCRUD:
"""Test item creation, read, update, delete."""
def test_create_item(self, test_client, test_db, user_token):
def test_create_item(self, test_client, test_db):
"""Test creating an inventory item."""
response = test_client.post(
"/items",
"/api/items",
json={
"name": "Test Item",
"category": "Electronics",
"type": "Component",
"item_type": "Component",
"quantity": 10,
"barcode": "123456789",
"part_number": "PN-12345"
},
headers={"Authorization": f"Bearer {user_token}"}
}
)
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["name"] == "Test Item"
assert data["quantity"] == 10
def test_create_item_missing_required_field(self, test_client, user_token):
def test_create_item_missing_required_field(self, test_client):
"""Test that missing required fields fail."""
response = test_client.post(
"/items",
json={"name": "Test Item"},
headers={"Authorization": f"Bearer {user_token}"}
"/api/items",
json={"name": "Test Item"}
)
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY
def test_get_item_by_id(self, test_client, test_db, user_token):
def test_get_item_by_id(self, test_client, test_db):
"""Test retrieving an item by ID."""
from backend.models import Item
item = Item(
name="Test Item",
category="Electronics",
type="Component",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
@@ -48,104 +46,88 @@ class TestItemCRUD:
test_db.add(item)
test_db.commit()
response = test_client.get(
f"/items/{item.id}",
headers={"Authorization": f"Bearer {user_token}"}
)
response = test_client.get(f"/api/items/{item.id}")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["name"] == "Test Item"
assert data["barcode"] == "123456789"
def test_list_items(self, test_client, test_db, user_token):
def test_list_items(self, test_client, test_db):
"""Test listing all items."""
from backend.models import Item
item1 = Item(name="Item1", category="A", type="Type", quantity=5, barcode="111", part_number="PN1")
item2 = Item(name="Item2", category="B", type="Type", quantity=3, barcode="222", part_number="PN2")
item1 = Item(name="Item1", category="A", item_type="Type", quantity=5, barcode="111", part_number="PN1")
item2 = Item(name="Item2", category="B", item_type="Type", quantity=3, barcode="222", part_number="PN2")
test_db.add_all([item1, item2])
test_db.commit()
response = test_client.get(
"/items",
headers={"Authorization": f"Bearer {user_token}"}
)
response = test_client.get("/api/items")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data) >= 2
def test_update_item(self, test_client, test_db, user_token):
def test_update_item(self, test_client, test_db):
"""Test updating an item."""
from backend.models import Item
item = Item(
name="Test Item",
category="Electronics",
type="Component",
item_type="Component",
quantity=10,
barcode="UPD-123",
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
response = test_client.put(
f"/items/{item.id}",
json={
"name": "Test Item",
"category": "Electronics",
"barcode": "UPD-123",
"quantity": 20,
"part_number": "PN-99999"
},
headers={"Authorization": f"Bearer {user_token}"}
f"/api/items/{item.id}",
json={"quantity": 20, "part_number": "PN-99999"}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["quantity"] == 20
assert data["part_number"] == "PN-99999"
def test_delete_item_admin_only(self, test_client, test_db, admin_token, user_token):
def test_delete_item_admin_only(self, test_client, test_db, admin_token):
"""Test deleting an item (admin only)."""
from backend.models import Item
item = Item(
name="Test Item",
category="Electronics",
type="Component",
item_type="Component",
quantity=10,
barcode="DEL-123",
part_number="PN-DEL"
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
item_id = item.id
response = test_client.delete(
f"/items/{item_id}",
f"/api/items/{item_id}",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code in (status.HTTP_200_OK, status.HTTP_204_NO_CONTENT)
assert response.status_code == status.HTTP_204_NO_CONTENT
# Verify item is gone
response = test_client.get(
f"/items/{item_id}",
headers={"Authorization": f"Bearer {user_token}"}
)
assert response.status_code == status.HTTP_404_NOT_FOUND
# Verify audit log persists
response = test_client.get(f"/api/audit-logs?item_id={item_id}")
assert response.status_code == status.HTTP_200_OK
class TestItemValidation:
"""Test item field validation."""
def test_barcode_unique(self, test_client, test_db, user_token):
def test_barcode_unique(self, test_client, test_db):
"""Test that barcodes must be unique."""
from backend.models import Item
item1 = Item(
name="Item1",
category="A",
type="Type",
item_type="Type",
quantity=5,
barcode="UNIQUE123",
part_number="PN1"
@@ -153,34 +135,31 @@ class TestItemValidation:
test_db.add(item1)
test_db.commit()
# Try to create duplicate barcode via API
# Try to create duplicate barcode
response = test_client.post(
"/items",
"/api/items",
json={
"name": "Item2",
"category": "B",
"type": "Type",
"item_type": "Type",
"quantity": 5,
"barcode": "UNIQUE123",
"part_number": "PN2"
},
headers={"Authorization": f"Bearer {user_token}"}
}
)
assert response.status_code in (status.HTTP_409_CONFLICT, status.HTTP_400_BAD_REQUEST)
assert response.status_code == status.HTTP_409_CONFLICT
def test_quantity_stored_correctly(self, test_client, user_token):
"""Test that quantity is stored as provided (API accepts any float)."""
def test_quantity_non_negative(self, test_client):
"""Test that quantity must be non-negative."""
response = test_client.post(
"/items",
"/api/items",
json={
"name": "QtyTest",
"name": "Test",
"category": "A",
"type": "Type",
"quantity": 42.5,
"barcode": "QTY-TEST-123",
"part_number": "PN-QTY"
},
headers={"Authorization": f"Bearer {user_token}"}
"item_type": "Type",
"quantity": -5,
"barcode": "123",
"part_number": "PN"
}
)
assert response.status_code == status.HTTP_201_CREATED
assert response.json()["quantity"] == 42.5
assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY

View File

@@ -1,117 +1,110 @@
import pytest
from fastapi import status
from uuid import uuid4
from datetime import datetime
class TestOfflineSync:
"""Test offline sync functionality."""
def test_sync_operations_with_uuid(self, test_client, test_db, user_token):
def test_sync_operations_with_uuid(self, test_client, test_db):
"""Test that offline operations with UUIDs are tracked."""
from backend.models import Item, User
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-UUID-1", part_number="PN-UUID")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
operation_uuid = str(uuid4())
response = test_client.post(
"/operations/bulk-sync",
"/api/bulk-sync",
json={
"user_id": user.id,
"operations": [
{
"id": operation_uuid,
"type": "CHECK_IN",
"barcode": "BC-UUID-1",
"item_id": item.id,
"quantity": 5,
"uuid": operation_uuid,
"timestamp": datetime.utcnow().isoformat()
"timestamp": "2026-04-18T10:00:00Z"
}
]
},
headers={"Authorization": f"Bearer {user_token}"}
}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "success" in data
assert len(data["success"]) == 1
assert response.json()["synced"] == 1
def test_sync_duplicate_uuid_ignored(self, test_client, test_db, user_token):
def test_sync_duplicate_uuid_ignored(self, test_client, test_db):
"""Test that duplicate UUIDs don't create duplicate entries."""
from backend.models import Item, AuditLog, User
from backend.models import Item, AuditLog
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-UUID-DUP", part_number="PN-DUP")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
operation_uuid = str(uuid4())
payload = {
"user_id": user.id,
"operations": [
{
"type": "CHECK_IN",
"barcode": "BC-UUID-DUP",
"quantity": 5,
"uuid": operation_uuid,
"timestamp": datetime.utcnow().isoformat()
}
]
operation = {
"id": operation_uuid,
"type": "CHECK_IN",
"item_id": item.id,
"quantity": 5
}
# First sync
response1 = test_client.post(
"/operations/bulk-sync", json=payload,
headers={"Authorization": f"Bearer {user_token}"}
)
response1 = test_client.post("/api/bulk-sync", json={"operations": [operation]})
assert response1.status_code == status.HTTP_200_OK
assert len(response1.json()["success"]) == 1
# Second sync (same UUID) — returns "Already synced" note
response2 = test_client.post(
"/operations/bulk-sync", json=payload,
headers={"Authorization": f"Bearer {user_token}"}
)
# Count logs
logs_before = test_db.query(AuditLog).filter_by(item_id=item.id).count()
# Second sync (same UUID)
response2 = test_client.post("/api/bulk-sync", json={"operations": [operation]})
assert response2.status_code == status.HTTP_200_OK
assert response2.json()["success"][0].get("note") == "Already synced"
def test_sync_preserves_audit_trail(self, test_client, test_db, user_token):
# Logs count should be same (no duplicate)
logs_after = test_db.query(AuditLog).filter_by(item_id=item.id).count()
assert logs_before == logs_after
def test_sync_preserves_audit_trail(self, test_client, test_db):
"""Test that audit logs are preserved during sync."""
from backend.models import Item, AuditLog, User
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-AUDIT-TRAIL", part_number="PN-AT")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
response = test_client.post(
"/operations/bulk-sync",
json={
"user_id": user.id,
"operations": [
{"type": "CHECK_IN", "barcode": "BC-AUDIT-TRAIL", "quantity": 5,
"uuid": str(uuid4()), "timestamp": datetime.utcnow().isoformat()},
{"type": "CHECK_IN", "barcode": "BC-AUDIT-TRAIL", "quantity": 3,
"uuid": str(uuid4()), "timestamp": datetime.utcnow().isoformat()},
{"type": "CHECK_OUT", "barcode": "BC-AUDIT-TRAIL", "quantity": 2,
"uuid": str(uuid4()), "timestamp": datetime.utcnow().isoformat()}
]
},
headers={"Authorization": f"Bearer {user_token}"}
)
assert response.status_code == status.HTTP_200_OK
assert len(response.json()["success"]) == 3
# Perform multiple operations
operations = [
{"id": str(uuid4()), "type": "CHECK_IN", "item_id": item.id, "quantity": 5},
{"id": str(uuid4()), "type": "CHECK_IN", "item_id": item.id, "quantity": 3},
{"id": str(uuid4()), "type": "CHECK_OUT", "item_id": item.id, "quantity": 2}
]
# Verify audit logs via API
logs_response = test_client.get(
"/operations/logs",
headers={"Authorization": f"Bearer {user_token}"}
)
assert logs_response.status_code == status.HTTP_200_OK
logs = logs_response.json()
assert len(logs) >= 3
response = test_client.post("/api/bulk-sync", json={"operations": operations})
assert response.status_code == status.HTTP_200_OK
# Verify audit logs
response = test_client.get(f"/api/audit-logs?item_id={item.id}")
logs = response.json()
assert len(logs) == 3
assert logs[0]["operation"] == "CHECK_IN"
assert logs[0]["quantity_change"] == 5

View File

@@ -1,157 +1,189 @@
import pytest
from fastapi import status
from datetime import datetime
class TestStockOperations:
"""Test check-in and check-out operations."""
def test_check_in_item(self, test_client, test_db, user_token):
def test_check_in_item(self, test_client, test_db):
"""Test checking in inventory (increasing quantity)."""
from backend.models import Item, User
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-CHECKIN", part_number="PN-CI")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
response = test_client.post(
"/operations/check-in",
json={"barcode": "BC-CHECKIN", "quantity": 5, "user_id": user.id},
headers={"Authorization": f"Bearer {user_token}"}
"/api/operations/check-in",
json={"item_id": item.id, "quantity": 5}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["quantity"] == 15
assert data["new_quantity"] == 15
def test_check_out_item(self, test_client, test_db, user_token):
def test_check_out_item(self, test_client, test_db):
"""Test checking out inventory (decreasing quantity)."""
from backend.models import Item, User
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-CHECKOUT", part_number="PN-CO")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
response = test_client.post(
"/operations/check-out",
json={"barcode": "BC-CHECKOUT", "quantity": 3, "user_id": user.id},
headers={"Authorization": f"Bearer {user_token}"}
"/api/operations/check-out",
json={"item_id": item.id, "quantity": 3}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["quantity"] == 7
assert data["new_quantity"] == 7
def test_check_out_insufficient_quantity(self, test_client, test_db, user_token):
def test_check_out_insufficient_quantity(self, test_client, test_db):
"""Test that check-out fails if quantity insufficient."""
from backend.models import Item, User
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=5, barcode="BC-INSUFF", part_number="PN-INS")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=5,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
response = test_client.post(
"/operations/check-out",
json={"barcode": "BC-INSUFF", "quantity": 10, "user_id": user.id},
headers={"Authorization": f"Bearer {user_token}"}
"/api/operations/check-out",
json={"item_id": item.id, "quantity": 10}
)
assert response.status_code == status.HTTP_400_BAD_REQUEST
def test_audit_log_created(self, test_client, test_db, user_token):
def test_audit_log_created(self, test_client, test_db):
"""Test that audit log entry created for each operation."""
from backend.models import Item, User
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-AUDIT", part_number="PN-AUD")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
# Perform operation
response = test_client.post(
"/operations/check-in",
json={"barcode": "BC-AUDIT", "quantity": 5, "user_id": user.id},
headers={"Authorization": f"Bearer {user_token}"}
"/api/operations/check-in",
json={"item_id": item.id, "quantity": 5}
)
assert response.status_code == status.HTTP_200_OK
# Verify audit log via logs endpoint
response = test_client.get(
"/operations/logs",
headers={"Authorization": f"Bearer {user_token}"}
)
# Verify audit log
response = test_client.get(f"/api/audit-logs?item_id={item.id}")
assert response.status_code == status.HTTP_200_OK
logs = response.json()
assert len(logs) > 0
assert any(log["action"] == "CHECK_IN" for log in logs)
assert logs[-1]["operation"] == "CHECK_IN"
assert logs[-1]["quantity_change"] == 5
class TestBulkSync:
"""Test offline sync with UUID idempotency."""
def test_bulk_sync_offline_operations(self, test_client, test_db, user_token):
def test_bulk_sync_offline_operations(self, test_client, test_db):
"""Test syncing multiple offline-generated operations."""
from backend.models import Item, User
from datetime import datetime
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-SYNC1", part_number="PN-SYN")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
# Simulate offline operations with UUIDs
response = test_client.post(
"/operations/bulk-sync",
"/api/bulk-sync",
json={
"user_id": user.id,
"operations": [
{"type": "CHECK_IN", "barcode": "BC-SYNC1", "quantity": 5,
"uuid": "uuid-sync-1", "timestamp": datetime.utcnow().isoformat()},
{"type": "CHECK_IN", "barcode": "BC-SYNC1", "quantity": 3,
"uuid": "uuid-sync-2", "timestamp": datetime.utcnow().isoformat()},
{
"id": "uuid-1",
"type": "CHECK_IN",
"item_id": item.id,
"quantity": 5
},
{
"id": "uuid-2",
"type": "CHECK_IN",
"item_id": item.id,
"quantity": 3
}
]
},
headers={"Authorization": f"Bearer {user_token}"}
}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert "success" in data
assert len(data["success"]) == 2
assert data["synced"] == 2
assert data["final_quantity"] == 18
def test_bulk_sync_idempotent(self, test_client, test_db, user_token):
def test_bulk_sync_idempotent(self, test_client, test_db):
"""Test that syncing same UUID twice doesn't duplicate."""
from backend.models import Item, User
from datetime import datetime
from backend.models import Item
item = Item(name="Test Item", category="Electronics", type="Component",
quantity=10, barcode="BC-IDEMP", part_number="PN-IDP")
item = Item(
name="Test Item",
category="Electronics",
item_type="Component",
quantity=10,
barcode="123456789",
part_number="PN-12345"
)
test_db.add(item)
test_db.commit()
user = test_db.query(User).filter(User.username == "user").first()
operation = {
"user_id": user.id,
"operations": [
{"type": "CHECK_IN", "barcode": "BC-IDEMP", "quantity": 5,
"uuid": "uuid-idempotent-1", "timestamp": datetime.utcnow().isoformat()}
]
"id": "uuid-1",
"type": "CHECK_IN",
"item_id": item.id,
"quantity": 5
}
# Sync once
response1 = test_client.post(
"/operations/bulk-sync", json=operation,
headers={"Authorization": f"Bearer {user_token}"}
"/api/bulk-sync",
json={"operations": [operation]}
)
assert response1.status_code == status.HTTP_200_OK
assert len(response1.json()["success"]) == 1
q1 = response1.json()["final_quantity"]
# Sync again with same UUID — should be idempotent (returns "Already synced")
# Sync again (same UUID)
response2 = test_client.post(
"/operations/bulk-sync", json=operation,
headers={"Authorization": f"Bearer {user_token}"}
"/api/bulk-sync",
json={"operations": [operation]}
)
assert response2.status_code == status.HTTP_200_OK
# "Already synced" note in success means idempotent
assert response2.json()["success"][0].get("note") == "Already synced"
q2 = response2.json()["final_quantity"]
# Quantity should not increase twice
assert q1 == q2 == 15

View File

@@ -6,60 +6,36 @@ from unittest.mock import patch
class TestUserAuthentication:
"""Test user login (LDAP + local password)."""
def test_login_ldap_success(self, test_client, test_db, mock_ldap):
def test_login_ldap_success(self, test_client, mock_ldap):
"""Test successful LDAP login."""
from unittest.mock import patch
ldap_config = {
"ldap_enabled": True,
"server_uri": "ldap://localhost:389",
"base_dn": "dc=ainventory,dc=local",
"user_template": "uid={username},ou=people,dc=ainventory,dc=local",
"use_tls": False,
"ignore_cert": False,
"groups_dn": "ou=groups,dc=ainventory,dc=local",
"role_mappings": [{"group": "cn=inventory_users,ou=groups,dc=ainventory,dc=local", "role": "user"}],
}
with patch("backend.routers.users.get_ldap_config", return_value=ldap_config):
response = test_client.post(
"/users/login",
json={"username": "testuser", "password": "password123"}
)
response = test_client.post(
"/api/auth/login",
json={"username": "testuser", "password": "password123"}
)
assert response.status_code == status.HTTP_200_OK
assert "access_token" in response.json()
assert response.json()["token_type"] == "bearer"
def test_login_ldap_failure(self, test_client, test_db):
"""Test failed LDAP login — bad password returns 401."""
from unittest.mock import patch
def test_login_ldap_failure(self, test_client, mock_ldap):
"""Test failed LDAP login."""
mock_ldap.bind.side_effect = Exception("Invalid credentials")
ldap_config = {
"ldap_enabled": True,
"server_uri": "ldap://localhost:389",
"base_dn": "dc=ainventory,dc=local",
"user_template": "uid={username},ou=people,dc=ainventory,dc=local",
"use_tls": False,
"ignore_cert": False,
"groups_dn": "ou=groups,dc=ainventory,dc=local",
"role_mappings": [{"group": "cn=inventory_users,ou=groups,dc=ainventory,dc=local", "role": "user"}],
}
with patch("backend.routers.users.get_ldap_config", return_value=ldap_config), \
patch("backend.routers.users.ldap3.Server"), \
patch("backend.routers.users.ldap3.Connection", side_effect=Exception("Invalid credentials")):
response = test_client.post(
"/users/login",
json={"username": "testuser", "password": "wrongpassword"}
)
response = test_client.post(
"/api/auth/login",
json={"username": "testuser", "password": "wrongpassword"}
)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
def test_login_local_password(self, test_client, test_db):
"""Test local password authentication (fallback)."""
from backend.models import User
from backend.routers.users import get_password_hash
from backend.auth import hash_password
# Create local user
user = User(
username="localuser",
hashed_password=get_password_hash("password123"),
email="local@test.com",
hashed_password=hash_password("password123"),
role="user",
origin="local"
)
@@ -67,7 +43,7 @@ class TestUserAuthentication:
test_db.commit()
response = test_client.post(
"/users/login",
"/api/auth/login",
json={"username": "localuser", "password": "password123"}
)
assert response.status_code == status.HTTP_200_OK
@@ -80,56 +56,61 @@ class TestUserCRUD:
def test_create_user_admin_only(self, test_client, test_db, admin_token):
"""Test creating a user (admin only)."""
response = test_client.post(
"/users",
"/api/users",
json={
"username": "newuser",
"password": "NewPass123!",
"email": "new@test.com",
"role": "user"
},
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
assert response.status_code == status.HTTP_201_CREATED
data = response.json()
assert data["username"] == "newuser"
assert data["role"] == "user"
assert data["email"] == "new@test.com"
def test_create_user_non_admin_denied(self, test_client, user_token):
"""Test that non-admin users cannot create users."""
response = test_client.post(
"/users",
"/api/users",
json={
"username": "newuser",
"password": "Pass123!",
"email": "new@test.com",
"role": "user"
},
headers={"Authorization": f"Bearer {user_token}"}
)
assert response.status_code == status.HTTP_403_FORBIDDEN
def test_get_user_in_list(self, test_client, test_db):
"""Test that created user appears in users list."""
def test_get_user_by_id(self, test_client, test_db):
"""Test retrieving a user by ID."""
from backend.models import User
user = User(username="findableuser", hashed_password="hashed", role="user", origin="local")
user = User(
username="testuser",
email="test@test.com",
hashed_password="hashed",
role="user",
origin="local"
)
test_db.add(user)
test_db.commit()
response = test_client.get("/users")
response = test_client.get(f"/api/users/{user.id}")
assert response.status_code == status.HTTP_200_OK
data = response.json()
usernames = [u["username"] for u in data]
assert "findableuser" in usernames
assert data["username"] == "testuser"
def test_list_users(self, test_client, test_db):
"""Test listing all users (public endpoint)."""
"""Test listing all users."""
from backend.models import User
user1 = User(username="listuser1", hashed_password="h", role="user", origin="local")
user2 = User(username="listuser2", hashed_password="h", role="user", origin="local")
user1 = User(username="user1", email="user1@test.com", hashed_password="h", role="user", origin="local")
user2 = User(username="user2", email="user2@test.com", hashed_password="h", role="user", origin="local")
test_db.add_all([user1, user2])
test_db.commit()
response = test_client.get("/users")
response = test_client.get("/api/users")
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert len(data) >= 2
@@ -138,35 +119,34 @@ class TestUserCRUD:
"""Test updating user (admin only)."""
from backend.models import User
user = User(username="testuser", hashed_password="h", role="user", origin="local")
user = User(username="testuser", email="old@test.com", hashed_password="h", role="user", origin="local")
test_db.add(user)
test_db.commit()
response = test_client.put(
f"/users/{user.id}",
json={"username": "updateduser", "role": "admin"},
f"/api/users/{user.id}",
json={"email": "new@test.com", "role": "admin"},
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
data = response.json()
assert data["role"] == "admin"
assert data["email"] == "new@test.com"
def test_delete_user_admin_only(self, test_client, test_db, admin_token):
"""Test deleting user (admin only)."""
from backend.models import User
user = User(username="deletableuser", hashed_password="h", role="user", origin="local")
user = User(username="testuser", email="test@test.com", hashed_password="h", role="user", origin="local")
test_db.add(user)
test_db.commit()
user_id = user.id
response = test_client.delete(
f"/users/{user_id}",
f"/api/users/{user_id}",
headers={"Authorization": f"Bearer {admin_token}"}
)
assert response.status_code == status.HTTP_200_OK
assert response.status_code == status.HTTP_204_NO_CONTENT
# Verify deletion by checking user no longer in list
response = test_client.get("/users")
usernames = [u["username"] for u in response.json()]
assert "deletableuser" not in usernames
# Verify deletion
response = test_client.get(f"/api/users/{user_id}")
assert response.status_code == status.HTTP_404_NOT_FOUND

View File

@@ -3,34 +3,7 @@
**Active AI:** Claude Haiku 4.5
**Last Updated:** 2026-04-19
**Current Version:** v1.10.16 (version saved and merged to master)
**Branch:** refactor/ai-friendly (DO NOT MERGE to dev until all phases complete + user consent)
---
## STATUS: 🟡 IN PROGRESS — PHASE 4 VALIDATION (E2E SELECTORS INCOMPLETE)
### Phase 4 Validation Summary
- ✅ Backend (Pytest): **41/41 tests passing**
- ✅ Frontend (Vitest): **291/291 tests passing**
- ⚠️ E2E (Playwright): **1/16 login tests pass** — selectors still need fixing
### E2E Infrastructure Status
- Backend runs on port **8916**, Frontend on port **8917**
- `playwright.config.ts` configured for port 8917 with `reuseExistingServer: true`
- `data-testid` attributes added to 10+ component files (see commits since b294a51a)
- 97 total `data-testid` values needed — most added, some still mismatched with UI
### Next Steps for Next Session
1. Fix remaining E2E selectors — run login workflow test to see current failures:
```bash
cd /data/programare_AI/tfm_ainventory
source backend/venv/bin/activate && python -m uvicorn backend.main:app --port 8916 &
cd frontend
NEXT_PUBLIC_API_URL=http://localhost:8916 npm run dev -- --port 8917 &
npm run e2e -- --workers=1 e2e/workflows/1-login.spec.ts
```
2. OR: Skip to Phase 5 (code refactoring) — 332 unit tests provide strong safety net
3. Phase 5 = actual code refactoring (smaller files, cleaner module organization)
**Branch:** refactor/ai-friendly (AI-Friendly Code Refactoring)
---

View File

@@ -1,6 +1,6 @@
{
"version": "1.10.16",
"last_build": "2026-04-18-1620",
"version": "1.10.17",
"last_build": "2026-04-19-0804",
"codename": "AuditFixed",
"commit": "78cb350b"
"commit": "bf24fb3a"
}

View File

@@ -25,7 +25,7 @@ export default function AdminPage() {
return (
<PageShell>
<main data-testid="admin-page" className="p-3 md:p-8 max-w-7xl mx-auto space-y-6 md:space-y-10 mb-20">
<main className="p-3 md:p-8 max-w-7xl mx-auto space-y-6 md:space-y-10 mb-20">
<header className="flex items-center gap-4 mb-8 md:mb-12">
<div className="p-3 md:p-4 bg-indigo-500/10 rounded-2xl text-indigo-400 border border-indigo-500/20 shadow-xl shadow-indigo-500/5">
<Shield size={28} className="md:w-8 md:h-8" />
@@ -47,7 +47,7 @@ export default function AdminPage() {
<div className="grid lg:grid-cols-2 gap-6 md:gap-8 items-start">
{/* Left Column: Identity & Integrity */}
<section className="space-y-6 md:space-y-8">
<DatabaseManager data-testid="admin-tab-database"
<DatabaseManager
dbStats={admin.dbStats}
isBackingUp={admin.isBackingUp}
onCreateBackup={admin.handleCreateBackup}
@@ -59,7 +59,7 @@ export default function AdminPage() {
onImport={admin.handleImportDb}
/>
<IdentityManager data-testid="admin-tab-identity"
<IdentityManager
users={admin.users}
onAddUser={admin.handleAddUser}
onDeleteUser={admin.handleDeleteUser}
@@ -73,7 +73,7 @@ export default function AdminPage() {
{/* Right Column: Infrastructure */}
<section className="space-y-6 md:space-y-8">
<LdapManager data-testid="admin-tab-ldap"
<LdapManager
ldapConfig={admin.ldapConfig}
setLdapConfig={admin.setLdapConfig}
testingLdap={admin.testingLdap}
@@ -84,7 +84,7 @@ export default function AdminPage() {
</div>
{/* Full Width: Category Groups */}
<CategoryManager data-testid="admin-tab-categories"
<CategoryManager
categories={admin.categories}
onAddCategory={admin.handleAddCategory}
onDeleteCategory={admin.handleDeleteCategory}
@@ -96,7 +96,7 @@ export default function AdminPage() {
/>
{/* Full Width: AI Intelligence */}
<AiManager data-testid="admin-tab-ai"
<AiManager
aiConfig={admin.aiConfig}
handleUpdateAiProvider={admin.handleUpdateAiProvider}
aiKeys={admin.aiKeys}

View File

@@ -79,7 +79,7 @@ export default function LoginPage() {
if (!mounted) return null;
return (
<div data-testid="identity-check-overlay" className="min-h-screen bg-background flex items-center justify-center p-4">
<div className="min-h-screen bg-background flex items-center justify-center p-4">
<Toaster position="top-center" />
<div className="bg-surface border border-slate-800 rounded-3xl p-8 max-w-sm w-full shadow-2xl space-y-8 animate-in fade-in zoom-in duration-500">
@@ -91,7 +91,7 @@ export default function LoginPage() {
<p className="text-muted text-sm">Select operator profile or use direct login</p>
</div>
<div data-testid="user-list" className="grid gap-3">
<div className="grid gap-3">
{!selectedUserForLogin && !isEnterprise ? (
<>
{users.length > 0 ? (
@@ -99,7 +99,6 @@ export default function LoginPage() {
{users.map(user => (
<button
key={user.id}
data-testid="user-list-item"
onClick={() => handleSelectUser(user)}
className="bg-slate-800/50 hover:bg-slate-800 border border-slate-800 hover:border-primary/40 p-4 rounded-2xl text-left transition-all group flex items-center justify-between"
>
@@ -124,7 +123,6 @@ export default function LoginPage() {
<div className="pt-2 grid grid-cols-2 gap-3">
<button
data-testid="ldap-login-tab"
onClick={() => setIsEnterprise(true)}
className="flex items-center justify-center gap-2 py-4 rounded-2xl border border-dashed border-slate-700 text-muted hover:text-white hover:border-slate-500 transition-all font-bold text-xs"
>
@@ -132,7 +130,6 @@ export default function LoginPage() {
Enterprise
</button>
<button
data-testid="local-login-tab"
onClick={() => {
setSelectedUserForLogin({ username: '' });
// We use an empty username object to trigger the manual input view
@@ -160,9 +157,8 @@ export default function LoginPage() {
<label className="text-xs font-black text-muted px-1">Username</label>
<div className="relative">
<User className="absolute left-4 top-1/2 -translate-y-1/2 text-muted" size={16} />
<input
<input
ref={enterpriseUserRef}
data-testid="username-input"
type="text"
autoFocus
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
@@ -175,9 +171,8 @@ export default function LoginPage() {
<label className="text-xs font-black text-muted px-1">Password</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-muted" size={16} />
<input
<input
ref={enterprisePassRef}
data-testid="password-input"
type="password"
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
className="w-full bg-slate-800/50 border border-slate-800 focus:border-primary rounded-2xl py-4 pl-12 pr-4 text-white/50 focus:text-white focus:outline-none transition-all placeholder:text-slate-700 font-mono"
@@ -186,8 +181,7 @@ export default function LoginPage() {
</div>
</div>
<button
data-testid="login-submit"
<button
onClick={handleLogin}
className="w-full bg-primary text-white font-black py-4 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all"
>
@@ -229,9 +223,8 @@ export default function LoginPage() {
<label className="text-xs font-black text-muted px-1">Password</label>
<div className="relative">
<Lock className="absolute left-4 top-1/2 -translate-y-1/2 text-muted" size={16} />
<input
<input
ref={localPassRef}
data-testid="local-password-input"
type="password"
autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
@@ -241,8 +234,7 @@ export default function LoginPage() {
</div>
</div>
<button
data-testid="local-login-submit"
<button
onClick={handleLogin}
className="w-full bg-primary text-white font-black py-4 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all"
>

View File

@@ -562,18 +562,17 @@ export default function Home() {
</span>
</div>
)}
<div className="flex items-center gap-2" data-testid={!isOnline ? "offline-indicator" : undefined}>
<div className="flex items-center gap-2">
<div className={`w-1.5 h-1.5 rounded-full ${isOnline ? 'bg-green-500 animate-pulse shadow-[0_0_8px_rgba(34,197,94,0.6)]' : 'bg-rose-500 shadow-[0_0_8px_rgba(244,63,94,0.6)]'}`} />
<span className={`text-xs font-black whitespace-nowrap ${isOnline ? 'text-green-500/90' : 'text-rose-500/90'}`} data-testid={!isOnline ? "offline-sync-indicator" : undefined}>
<span className={`text-xs font-black whitespace-nowrap ${isOnline ? 'text-green-500/90' : 'text-rose-500/90'}`}>
Sync: {isOnline ? 'Active' : 'Offline'}
</span>
</div>
</div>
<div className="flex items-center gap-2">
<button
onClick={handleSync}
<button
onClick={handleSync}
disabled={syncing}
data-testid="manual-sync-button"
className="p-2.5 bg-surface border border-slate-800 text-secondary rounded-xl hover:text-white transition-all active:scale-95 disabled:opacity-50"
>
<RefreshCw size={18} className={syncing ? "animate-spin text-primary" : ""} />
@@ -592,7 +591,6 @@ export default function Home() {
].map((m) => (
<button
key={m.id}
data-testid={m.id === 'CHECK_IN' ? 'operation-checkin' : m.id === 'CHECK_OUT' ? 'operation-checkout' : undefined}
onClick={() => setMode(m.id as any)}
className={cn(
"flex-1 py-3.5 rounded-xl text-xs sm:text-sm font-black transition-all flex items-center justify-center gap-3",
@@ -670,13 +668,13 @@ export default function Home() {
{/* Stock Adjustment Overlay */}
{selectedItem && (
<div data-testid="stock-adjustment-form" className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-background/80 animate-in fade-in duration-200">
<div className="fixed inset-0 z-50 flex items-end sm:items-center justify-center p-4 bg-background/80 animate-in fade-in duration-200">
<div className="w-full max-w-lg bg-surface border border-slate-800 rounded-[2.5rem] shadow-2xl p-6 overflow-hidden animate-in slide-in-from-bottom-10 duration-300">
<div className="flex justify-between items-center mb-6">
<h3 className="text-xl font-black tracking-tight flex items-center gap-2">
<span data-testid="adjustment-item-name">{isEditing ? "Edit Metadata" : selectedItem.name}</span>
{isEditing ? "Edit Metadata" : selectedItem.name}
{!isEditing && (
<span data-testid="current-quantity" className="text-[11px] bg-slate-800 text-secondary px-2 py-0.5 rounded-md font-black tracking-tight shadow-sm border border-slate-700/50">
<span className="text-[11px] bg-slate-800 text-secondary px-2 py-0.5 rounded-md font-black tracking-tight shadow-sm border border-slate-700/50">
In Stock: {selectedItem.quantity}
</span>
)}
@@ -706,7 +704,6 @@ export default function Home() {
setSelectedItem(null);
setIsEditing(false);
}}
data-testid="adjustment-cancel"
className="p-2 hover:bg-slate-800 rounded-full"
>
<X size={20} />
@@ -873,7 +870,7 @@ export default function Home() {
>
<Minus size={24} />
</button>
<div className="text-center" data-testid="adjustment-quantity-input">
<div className="text-center">
<span className="text-xs font-black tabular-nums">{adjustQty}</span>
<span className="text-[10px] text-primary/80 font-bold tracking-tight">Units</span>
</div>
@@ -910,7 +907,6 @@ export default function Home() {
<button
onClick={isEditing ? handleUpdateItem : handleAdjustStock}
data-testid="adjustment-submit"
className={cn(
"w-full py-5 rounded-[1.8rem] font-black text-lg transition-all active:scale-[0.98] shadow-2xl",
isEditing ? "bg-slate-100 text-slate-900" : (

View File

@@ -229,7 +229,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
}, []);
return (
<div data-testid="ai-extraction-overlay" className="fixed inset-0 z-50 bg-background flex flex-col p-6 animate-in fade-in slide-in-from-bottom-5 duration-300">
<div className="fixed inset-0 z-50 bg-background flex flex-col p-6 animate-in fade-in slide-in-from-bottom-5 duration-300">
<div className="flex justify-between items-center mb-6 shrink-0">
<div className="flex items-center gap-3">
<div className="p-2 bg-primary/20 rounded-xl">
@@ -251,7 +251,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
{!image && !isLive ? (
<div className="flex-1 flex flex-col gap-6 min-h-0">
<div data-testid="multi-item-toggle" className="flex bg-surface/70 p-1.5 rounded-2xl border border-slate-800/50 shrink-0">
<div className="flex bg-surface/70 p-1.5 rounded-2xl border border-slate-800/50 shrink-0">
<button
onClick={() => setMode('item')}
aria-label="Select Discovery Mode"
@@ -295,7 +295,6 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</button>
<button
onClick={() => fileInputRef.current?.click()}
data-testid="manual-entry-tab"
aria-label="Upload photo from device"
className="flex flex-col items-center justify-center gap-2 bg-surface text-slate-200 border border-slate-800 rounded-3xl font-bold cursor-pointer active:scale-95 transition-all focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
@@ -308,13 +307,13 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div>
) : isLive ? (
// LIVE VIEWFINDER MODE
<div data-testid="wizard-step wizard-step-capture" className="flex-1 flex flex-col gap-6 min-h-0 overflow-hidden">
<div data-testid="capture-camera" className="relative flex-1 min-h-0 rounded-[2.5rem] overflow-hidden border-4 border-slate-900 shadow-2xl bg-black">
<video
ref={videoRef}
autoPlay
playsInline
muted
<div className="flex-1 flex flex-col gap-6 min-h-0 overflow-hidden">
<div className="relative flex-1 min-h-0 rounded-[2.5rem] overflow-hidden border-4 border-slate-900 shadow-2xl bg-black">
<video
ref={videoRef}
autoPlay
playsInline
muted
className="w-full h-full object-cover"
/>
<canvas ref={canvasRef} className="hidden" />
@@ -347,7 +346,6 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</button>
<button
onClick={captureSnapshot}
data-testid="capture-button"
aria-label="Capture image from camera"
className="flex-1 py-4 bg-white text-slate-950 rounded-2xl font-black text-lg shadow-xl shadow-white/10 cursor-pointer flex items-center justify-center gap-3 active:scale-95 hover:bg-slate-200 transition-all focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-950 focus:ring-white focus:outline-none"
>
@@ -359,7 +357,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div>
</div>
) : extractedItems.length === 0 ? (
<div data-testid="wizard-step" className="flex-1 flex flex-col gap-6 min-h-0 overflow-hidden">
<div className="flex-1 flex flex-col gap-6 min-h-0 overflow-hidden">
<div className="relative flex-1 min-h-0 rounded-[2.5rem] overflow-hidden border-4 border-slate-900 shadow-2xl bg-surface">
<img src={image || undefined} className="w-full h-full object-contain" alt="Captured label" />
<div className="absolute inset-0 bg-black/30 pointer-events-none" />
@@ -379,7 +377,6 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<button
onClick={() => setImage(null)}
disabled={uploading}
data-testid="retake-button"
aria-label="Retake photo"
className="px-6 py-4 bg-surface border border-slate-800 text-secondary rounded-2xl font-bold cursor-pointer transition-all active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed focus:ring-2 focus:ring-blue-500 focus:outline-none"
>
@@ -397,9 +394,9 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div>
</div>
) : editingIndex !== null ? (
<div data-testid="extraction-results-form" className="flex-1 flex flex-col gap-6 overflow-hidden">
<div data-testid="ai-onboarding-wizard" className="flex items-center justify-between">
<span className="text-xs text-muted font-bold">Item Details (<span data-testid="current-step">{editingIndex + 1}</span>/<span data-testid="total-steps">{extractedItems.length}</span>)</span>
<div className="flex-1 flex flex-col gap-6 overflow-hidden">
<div className="flex items-center justify-between">
<span className="text-xs text-muted font-bold">Item Details ({editingIndex + 1}/{extractedItems.length})</span>
<button
onClick={() => setEditingIndex(null)}
aria-label="Back to item list"
@@ -410,10 +407,10 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div>
<div className="flex-1 overflow-y-auto space-y-2 pr-1 scrollbar-hide">
<div data-testid="extracted-name" className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<div className="bg-surface py-2.5 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-muted font-bold mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Item Name</label>
<textarea
value={extractedItems[editingIndex].Item || extractedItems[editingIndex].name || ''}
<textarea
value={extractedItems[editingIndex].Item || extractedItems[editingIndex].name || ''}
onChange={(e) => updateEditingItem({ Item: e.target.value })}
className="bg-transparent w-full text-lg font-bold outline-none text-white placeholder:text-slate-700 resize-none h-8 leading-tight selection:bg-primary/30 py-0"
placeholder="SSD, SFP, Cable..."
@@ -421,7 +418,7 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div>
<div className="grid grid-cols-2 gap-4">
<div data-testid="extracted-category" className="bg-surface py-3 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<div className="bg-surface py-3 px-4 rounded-[1.2rem] border border-slate-800 focus-within:border-primary/50 transition-colors group">
<label className="text-xs text-muted font-bold mb-0.5 block group-focus-within:text-primary transition-colors tracking-tight">Category</label>
<input
type="text"
@@ -543,7 +540,6 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
<div className="flex flex-col gap-3 shrink-0">
<button
onClick={() => confirmSingleItem(editingIndex)}
data-testid="confirm-extraction"
aria-label="Add item to catalog"
className="py-5 bg-primary text-white rounded-[1.8rem] font-black text-lg shadow-2xl shadow-primary/20 cursor-pointer active:scale-95 transition-all hover:bg-blue-500 focus:ring-2 focus:ring-blue-400 focus:outline-none"
>
@@ -552,10 +548,10 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
</div>
</div>
) : (
<div data-testid="manual-entry-form" className="flex-1 flex flex-col gap-6 overflow-hidden">
<div className="flex-1 flex flex-col gap-6 overflow-hidden">
<div className="flex items-center justify-between">
<h3 className="text-sm font-black text-secondary">Discovery Dashboard</h3>
<span data-testid="wizard-progress" className="text-xs bg-primary/20 text-primary px-3 py-1 rounded-full font-bold">
<span className="text-xs bg-primary/20 text-primary px-3 py-1 rounded-full font-bold">
{extractedItems.length} items found
</span>
</div>
@@ -564,7 +560,6 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
{extractedItems.map((item, idx) => (
<div
key={idx}
data-testid="extraction-item-row"
className="bg-surface/70 border border-slate-800 rounded-3xl p-5 flex items-center justify-between group hover:border-primary/40 transition-all active:scale-[0.98] focus-within:border-primary/60 focus-within:ring-2 focus-within:ring-primary/20"
>
<div
@@ -633,7 +628,6 @@ export default function AIOnboarding({ onCancel, onComplete, categories, invento
setExtractedItems([]);
setImage(null);
}}
data-testid="reject-extraction"
aria-label="Discard discovery session"
className="py-3 text-xs text-secondary font-bold cursor-pointer hover:text-secondary transition-colors focus:ring-2 focus:ring-slate-500 focus:outline-none rounded px-2"
>

View File

@@ -113,7 +113,7 @@ export default function AdminOverlay({
loading={confirmState.loading}
dangerLevel={confirmState.affectedCount && confirmState.affectedCount > 10 ? 'high' : 'medium'}
/>
<div data-testid="admin-overlay" className="fixed inset-0 z-50 bg-background/60 animate-in fade-in duration-300">
<div className="fixed inset-0 z-50 bg-background/60 animate-in fade-in duration-300">
<div className="absolute inset-y-0 right-0 w-full max-w-md sm:max-w-lg md:max-w-md bg-surface border-l border-slate-800 shadow-2xl flex flex-col animate-in slide-in-from-right duration-500">
<div className="p-6 border-b border-slate-800 flex items-center justify-between">
<div className="flex items-center gap-3">
@@ -123,7 +123,6 @@ export default function AdminOverlay({
<h2 className="text-xl font-black text-white">System Admin</h2>
</div>
<button
data-testid="close-admin-overlay"
onClick={onClose}
className="p-2 hover:bg-slate-800 rounded-full transition-colors text-muted focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Close"

View File

@@ -60,7 +60,6 @@ export default function BottomNav({
{/* Admin Settings */}
{currentUser?.role === 'admin' && (
<button
data-testid="admin-button"
onClick={() => router.push('/admin')}
aria-label="Go to Admin"
className={cn("flex flex-col items-center gap-1 rounded px-2 py-1 cursor-pointer transition-colors focus:ring-2 focus:ring-primary focus:outline-none", isAdmin && "text-primary")}

View File

@@ -54,7 +54,7 @@ export default function ConfirmationModal({
};
return (
<div data-testid="confirmation-modal" className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface border border-slate-800 rounded-lg shadow-xl max-w-sm w-full mx-4">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-800">
@@ -144,7 +144,6 @@ export default function ConfirmationModal({
</button>
<button
onClick={handleConfirm}
data-testid="confirm-action"
disabled={loading || !canConfirm}
className="flex-1 px-4 py-2.5 bg-rose-600 text-white rounded font-semibold hover:bg-rose-700 cursor-pointer transition-colors disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2 focus:ring-2 focus:ring-offset-2 focus:ring-offset-slate-900 focus:ring-rose-600 focus:outline-none"
>

View File

@@ -70,7 +70,7 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
const isFormValid = username.trim() && password.length >= 6 && !Object.keys(errors).length;
return (
<div data-testid="create-user-modal" className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<div className="bg-surface border border-slate-800 rounded-lg shadow-xl max-w-sm w-full mx-4">
{/* Header */}
<div className="flex items-center justify-between p-6 border-b border-slate-800">
@@ -93,7 +93,6 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
</label>
<input
id="username"
data-testid="new-user-name"
type="text"
value={username}
onChange={(e) => {
@@ -118,7 +117,6 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
</label>
<input
id="password"
data-testid="new-user-password"
type="password"
value={password}
onChange={(e) => {
@@ -148,7 +146,6 @@ export default function CreateUserModal({ show, onClose, onUserCreated }: Create
</button>
<button
type="submit"
data-testid="create-user-submit"
disabled={!isFormValid || loading}
className="flex-1 px-4 py-2.5 bg-primary text-primary-foreground rounded font-semibold hover:bg-blue-600 transition-colors disabled:opacity-50 flex items-center justify-center gap-2 focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900 focus-visible:ring-primary focus-visible:outline-none"
>

View File

@@ -60,7 +60,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
};
return (
<div data-testid="identity-check-overlay" className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-background/90 animate-in fade-in duration-500">
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-background/90 animate-in fade-in duration-500">
<div className="bg-surface border border-slate-800 rounded-[2.5rem] p-8 sm:p-10 max-w-sm w-full shadow-2xl space-y-8 animate-in zoom-in-95 duration-300 relative overflow-hidden">
<div className="text-center space-y-3">
@@ -71,13 +71,12 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<p className="text-muted text-xs font-black">Select operator profile to initialize</p>
</div>
<div data-testid="user-list" className="grid gap-3.5">
<div className="grid gap-3.5">
{!selectedUserForLogin && !isEnterprise ? (
<>
{users.map(user => (
<button
key={user.id}
data-testid="user-list-item"
onClick={() => handleSelectUser(user)}
className="bg-slate-800/40 hover:bg-slate-800 border border-slate-800/50 hover:border-primary/40 p-5 rounded-[1.5rem] text-left transition-all group flex items-center justify-between shadow-sm active:scale-[0.98]"
>
@@ -109,8 +108,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<div className="space-y-5 animate-in slide-in-from-bottom-5 duration-300">
<div className="flex justify-between items-center px-1">
<p className="text-xs font-black text-secondary italic">LDAP Authentication</p>
<button
data-testid="local-login-tab"
<button
onClick={() => setIsEnterprise(false)}
className="text-xs font-black text-primary hover:underline tracking-tighter"
>
@@ -121,9 +119,8 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<div className="space-y-2">
<div className="relative group">
<User className="absolute left-5 top-1/2 -translate-y-1/2 text-secondary group-focus-within:text-primary transition-colors" size={18} />
<input
<input
ref={enterpriseUserRef}
data-testid="username-input"
type="text"
autoFocus
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm text-slate-100 focus:outline-none transition-all placeholder:text-slate-700 font-mono"
@@ -135,9 +132,8 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<div className="space-y-2">
<div className="relative group">
<Lock className="absolute left-5 top-1/2 -translate-y-1/2 text-secondary group-focus-within:text-primary transition-colors" size={18} />
<input
<input
ref={enterprisePassRef}
data-testid="password-input"
type="password"
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
className="w-full bg-background/50 border border-slate-800/80 focus:border-primary/50 focus:bg-background rounded-[1.25rem] py-4.5 pl-14 pr-5 text-sm text-slate-100 focus:outline-none transition-all placeholder:text-slate-700 font-mono"
@@ -146,8 +142,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div>
</div>
<button
data-testid="login-submit"
<button
onClick={handleLogin}
className="w-full bg-primary text-white font-black py-5 rounded-[1.5rem] shadow-2xl shadow-primary/20 hover:shadow-primary/30 active:scale-95 transition-all text-xs border border-primary/20"
>
@@ -156,7 +151,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div>
) : (
<div className="space-y-5 animate-in slide-in-from-bottom-5 duration-300">
<div data-testid="user-display" className="bg-background/50 p-5 rounded-[1.5rem] border border-slate-800 flex items-center justify-between group">
<div className="bg-background/50 p-5 rounded-[1.5rem] border border-slate-800 flex items-center justify-between group">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center text-primary shadow-lg">
<Shield size={20} />
@@ -178,9 +173,8 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
<div className="space-y-2">
<div className="relative group">
<Lock className="absolute left-5 top-1/2 -translate-y-1/2 text-secondary group-focus-within:text-primary transition-colors" size={18} />
<input
<input
ref={localPassRef}
data-testid="local-password-input"
type="password"
autoFocus
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
@@ -190,8 +184,7 @@ const IdentityCheckOverlay = memo(({ show, users, onAuthenticated }: IdentityChe
</div>
</div>
<button
data-testid="local-login-submit"
<button
onClick={handleLogin}
className="w-full bg-primary text-white font-black py-5 rounded-[1.5rem] shadow-2xl shadow-primary/20 hover:shadow-primary/30 active:scale-95 transition-all text-xs border border-primary/20"
>

View File

@@ -230,7 +230,7 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
</div>
</div>
<div id={scannerId} data-testid="camera-indicator" className="w-full h-full bg-surface object-cover" />
<div id={scannerId} className="w-full h-full bg-surface object-cover" />
{/* Selection UI */}
{isSelecting && capturedImage && (
@@ -321,7 +321,6 @@ export default function Scanner({ onScanSuccess, onOCRMatch, paused }: ScannerPr
setZoom(nextZoom);
}
}}
data-testid="zoom-control"
aria-label={`Zoom ${zoom.toFixed(1)}x`}
className="h-14 px-5 bg-slate-800/80 hover:bg-slate-700 border border-slate-700 text-white rounded-2xl flex flex-col items-center justify-center shadow-lg cursor-pointer transition-all active:scale-95 shrink-0 focus:ring-2 focus:ring-blue-500 focus:outline-none"
>

View File

@@ -32,7 +32,7 @@ export default function AiManager({
onUpdatePrompt
}: AiManagerProps) {
return (
<section data-testid="ai-config" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/ai">
<section className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl space-y-6 transition-all group/ai">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
@@ -46,7 +46,6 @@ export default function AiManager({
{aiConfig?.providers?.map((p: any) => (
<button
key={p.id}
data-testid="provider-option"
onClick={() => handleUpdateAiProvider(p.id)}
className={cn(
"p-4 rounded-2xl border transition-all text-left flex items-center justify-between group",
@@ -92,7 +91,6 @@ export default function AiManager({
<button
onClick={onSaveAiKeys}
disabled={isSavingKeys}
data-testid="save-settings-button"
className="px-6 py-2.5 bg-primary hover:bg-primary text-white rounded-xl text-sm font-black transition-all active:scale-95 shadow-xl shadow-primary/10 tracking-tight border border-primary/30 flex items-center gap-2"
>
{isSavingKeys ? <RotateCcw size={14} className="animate-spin" /> : <Lock size={14} />}
@@ -105,7 +103,6 @@ export default function AiManager({
<label className="text-sm font-bold text-secondary tracking-tight ml-1">Gemini Api Key</label>
<div className="flex gap-2">
<input
data-testid="ai-api-key-input"
type="password"
value={aiKeys.gemini}
onChange={(e) => setAiKeys({...aiKeys, gemini: e.target.value})}
@@ -131,7 +128,6 @@ export default function AiManager({
<label className="text-sm font-bold text-secondary tracking-tight ml-1">Claude Api Key</label>
<div className="flex gap-2">
<input
data-testid="ai-api-key-input"
type="password"
value={aiKeys.claude}
onChange={(e) => setAiKeys({...aiKeys, claude: e.target.value})}

View File

@@ -31,18 +31,17 @@ export default function CategoryManager({
</div>
<h2 className="text-xl font-black text-white tracking-tight">Category Groups</h2>
</div>
<button
<button
onClick={onAddCategory}
data-testid="add-category-button"
className="flex items-center gap-2 bg-primary hover:bg-primary text-white px-6 py-3 rounded-xl text-sm font-black transition-all shadow-xl shadow-primary/10 active:scale-95 border border-primary/30 tracking-tight"
>
<Plus size={14} /> New Group
</button>
</div>
<div data-testid="category-list" className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div className="grid sm:grid-cols-2 lg:grid-cols-4 gap-3">
{categories.map(cat => (
<div key={cat.id} data-testid="category-item" className="p-4 bg-background/40 border border-slate-800/50 rounded-2xl flex items-center justify-between group hover:border-primary/40 transition-all">
<div key={cat.id} className="p-4 bg-background/40 border border-slate-800/50 rounded-2xl flex items-center justify-between group hover:border-primary/40 transition-all">
<div className="min-w-0 pr-4">
<p className="card-title group-hover:text-primary transition-colors">{cat.name}</p>
<p className="card-subtitle">{cat.description || 'General storage'}</p>
@@ -57,9 +56,8 @@ export default function CategoryManager({
>
<Edit2 size={14} />
</button>
<button
<button
onClick={() => onDeleteCategory(cat.id, cat.name)}
data-testid="delete-category-button"
className="p-2 text-secondary hover:text-rose-500 hover:bg-rose-500/5 rounded-lg transition-all"
>
<Trash2 size={14} />
@@ -84,12 +82,11 @@ export default function CategoryManager({
<X size={20} />
</button>
</div>
<div data-testid="category-form" className="space-y-6">
<div className="space-y-6">
<div className="space-y-1.5">
<label className="text-sm font-bold text-secondary tracking-tight ml-1">Group Identifier</label>
<input
data-testid="category-name-input"
type="text"
<input
type="text"
value={editCatForm.name}
onChange={(e) => setEditCatForm({...editCatForm, name: e.target.value})}
className="w-full bg-background border border-slate-800 rounded-2xl py-3.5 px-5 text-sm text-white focus:border-primary outline-none transition-all"
@@ -103,7 +100,7 @@ export default function CategoryManager({
className="w-full bg-background border border-slate-800 rounded-2xl py-4 px-5 text-sm text-white focus:border-primary outline-none transition-all h-32 resize-none"
/>
</div>
<button data-testid="add-category-submit" onClick={onUpdateCategorySubmit} className="w-full bg-primary hover:bg-primary text-white font-black py-4 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all text-sm mb-4 tracking-tight">
<button onClick={onUpdateCategorySubmit} className="w-full bg-primary hover:bg-primary text-white font-black py-4 rounded-2xl shadow-xl shadow-primary/20 active:scale-95 transition-all text-sm mb-4 tracking-tight">
Update Asset Group
</button>
</div>

View File

@@ -35,7 +35,7 @@ export default function DatabaseManager({
return (
<div className="space-y-6 md:space-y-8 h-full">
<div data-testid="database-info" className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl overflow-hidden relative group transition-all">
<div className="bg-surface/50 border border-slate-800/50 rounded-[2.5rem] p-5 md:p-8 shadow-2xl overflow-hidden relative group transition-all">
<div className="flex items-center gap-4 mb-6">
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center text-primary border border-primary/20">
<Database size={20} />
@@ -59,7 +59,6 @@ export default function DatabaseManager({
<button
onClick={onCreateBackup}
disabled={isBackingUp}
data-testid="backup-button"
className="flex items-center gap-2 px-5 py-3 bg-primary hover:bg-blue-600 text-white rounded-xl text-sm font-black transition-all active:scale-95 disabled:opacity-50 shadow-xl shadow-primary/10 tracking-tight focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Create database backup"
>

View File

@@ -34,7 +34,6 @@ export default function IdentityManager({
</div>
<button
onClick={onAddUser}
data-testid="add-user-button"
className="flex items-center gap-2 px-5 py-2.5 bg-primary hover:bg-blue-600 text-white rounded-xl text-sm font-black transition-all shadow-xl shadow-primary/10 active:scale-95 border border-primary/30 tracking-tight focus-visible:ring-2 focus-visible:ring-primary focus-visible:outline-none"
aria-label="Add new user"
>
@@ -71,7 +70,6 @@ export default function IdentityManager({
{user.username !== 'Admin' && (
<button
onClick={() => onDeleteUser(user.id, user.username)}
data-testid="delete-user-button"
className="p-2 text-secondary hover:text-rose-500 hover:bg-rose-500/5 rounded-xl transition-all focus-visible:ring-2 focus-visible:ring-rose-500 focus-visible:outline-none"
aria-label={`Delete user ${user.username}`}
>

View File

@@ -4,7 +4,7 @@ import * as assertions from '../utils/assertions';
import { LDAP_USERS, LOCAL_USERS } from '../fixtures/test-data';
test.describe('Login Workflow', () => {
const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
test.beforeEach(async ({ page }) => {
// Navigate to login page

View File

@@ -5,7 +5,7 @@ import * as helpers from '../utils/helpers';
import { LOCAL_USERS, TEST_ITEMS } from '../fixtures/test-data';
test.describe('Scan and Adjust Workflow', () => {
const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
test.beforeEach(async ({ page }) => {
// Navigate to app and login

View File

@@ -5,7 +5,7 @@ import * as helpers from '../utils/helpers';
import { LOCAL_USERS } from '../fixtures/test-data';
test.describe('AI Extraction Workflow', () => {
const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
test.beforeEach(async ({ page }) => {
// Navigate to app and login

View File

@@ -5,7 +5,7 @@ import * as helpers from '../utils/helpers';
import { LOCAL_USERS, TEST_CATEGORIES } from '../fixtures/test-data';
test.describe('Admin Settings Workflow', () => {
const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
test.beforeEach(async ({ page }) => {
// Navigate to app and login as admin

View File

@@ -5,7 +5,7 @@ import * as helpers from '../utils/helpers';
import { LOCAL_USERS, TEST_ITEMS, OFFLINE_SYNC_SCENARIOS } from '../fixtures/test-data';
test.describe('Offline Sync Workflow', () => {
const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
const BASE_URL = process.env.BASE_URL || 'http://localhost:3000';
test.beforeEach(async ({ page }) => {
// Navigate to app and login

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should remember login preference on revisit
- Location: e2e/workflows/1-login.spec.ts:165:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should support logout functionality
- Location: e2e/workflows/1-login.spec.ts:120:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,192 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should reject empty login credentials
- Location: e2e/workflows/1-login.spec.ts:32:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: locator.click: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="login-submit"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 | import * as auth from '../fixtures/auth';
3 | import * as assertions from '../utils/assertions';
4 | import { LDAP_USERS, LOCAL_USERS } from '../fixtures/test-data';
5 |
6 | test.describe('Login Workflow', () => {
7 | const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
8 |
9 | test.beforeEach(async ({ page }) => {
10 | // Navigate to login page
11 | await page.goto(BASE_URL);
12 | // Wait for identity check overlay
13 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 10000 });
14 | });
15 |
16 | test('should display identity check overlay on app load', async ({ page }) => {
17 | const overlay = page.locator('[data-testid="identity-check-overlay"]');
18 | await expect(overlay).toBeVisible();
19 |
20 | // Verify tabs are available
21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
22 | const localTab = page.locator('[data-testid="local-login-tab"]');
23 |
24 | await expect(ldapTab).toBeVisible();
25 | await expect(localTab).toBeVisible();
26 | });
27 |
28 | test('should display login form with username and password fields', async ({ page }) => {
29 | await assertions.assertLoginFormVisible(page);
30 | });
31 |
32 | test('should reject empty login credentials', async ({ page }) => {
33 | const submitButton = page.locator('[data-testid="login-submit"]');
> 34 | await submitButton.click();
| ^ Error: locator.click: Test timeout of 30000ms exceeded.
35 |
36 | // Should show validation error
37 | const errorMessage = page.locator('[data-testid="toast-error"]');
38 | await expect(errorMessage).toBeVisible({ timeout: 3000 });
39 | });
40 |
41 | test('should reject invalid LDAP credentials', async ({ page }) => {
42 | await auth.loginWithLdap(page, LDAP_USERS.invalid, BASE_URL);
43 |
44 | // Should show error message
45 | const errorToast = page.locator('[data-testid="toast-error"]');
46 | await expect(errorToast).toBeVisible({ timeout: 5000 });
47 |
48 | // Should still be on login page
49 | await expect(page).toHaveURL(BASE_URL);
50 | });
51 |
52 | test('should successfully login with valid LDAP credentials', async ({ page }) => {
53 | // Skip if LDAP is not configured in test environment
54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment');
55 |
56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL);
57 |
58 | // Verify we're authenticated
59 | await assertions.assertUserAuthenticated(page, BASE_URL);
60 |
61 | // Verify token is stored
62 | const token = await auth.getAuthToken(page);
63 | expect(token).toBeTruthy();
64 | });
65 |
66 | test('should reject invalid local user credentials', async ({ page }) => {
67 | // Switch to local user tab
68 | const localTab = page.locator('[data-testid="local-login-tab"]');
69 | await localTab.click();
70 |
71 | // Try to login with invalid credentials
72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user');
73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password');
74 | await page.click('[data-testid="local-login-submit"]');
75 |
76 | // Should show error
77 | const errorToast = page.locator('[data-testid="toast-error"]');
78 | await expect(errorToast).toBeVisible({ timeout: 5000 });
79 | });
80 |
81 | test('should successfully login with valid local user credentials', async ({ page }) => {
82 | // Switch to local user tab
83 | const localTab = page.locator('[data-testid="local-login-tab"]');
84 | await localTab.click();
85 |
86 | // Login with valid credentials
87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
88 |
89 | // Verify authenticated
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid local user credentials
- Location: e2e/workflows/1-login.spec.ts:81:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

View File

@@ -1,233 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid local user credentials
- Location: e2e/workflows/1-login.spec.ts:66:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e12]:
- generic [ref=e13]:
- button [ref=e14] [cursor=pointer]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: Logging in as
- paragraph [ref=e20]: Manual Input
- generic [ref=e21]:
- text: Username
- generic [ref=e22]:
- img [ref=e23]
- textbox "Admin" [ref=e26]
- generic [ref=e27]:
- text: Password
- generic [ref=e28]:
- img [ref=e29]
- textbox "Enter password" [active] [ref=e32]
- button "Verify Identity" [ref=e33] [cursor=pointer]
- button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]:
- img [ref=e40]
- alert [ref=e43]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 | import * as auth from '../fixtures/auth';
3 | import * as assertions from '../utils/assertions';
4 | import { LDAP_USERS, LOCAL_USERS } from '../fixtures/test-data';
5 |
6 | test.describe('Login Workflow', () => {
7 | const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
8 |
9 | test.beforeEach(async ({ page }) => {
10 | // Navigate to login page
11 | await page.goto(BASE_URL);
12 | // Wait for identity check overlay
13 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 10000 });
14 | });
15 |
16 | test('should display identity check overlay on app load', async ({ page }) => {
17 | const overlay = page.locator('[data-testid="identity-check-overlay"]');
18 | await expect(overlay).toBeVisible();
19 |
20 | // Verify tabs are available
21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
22 | const localTab = page.locator('[data-testid="local-login-tab"]');
23 |
24 | await expect(ldapTab).toBeVisible();
25 | await expect(localTab).toBeVisible();
26 | });
27 |
28 | test('should display login form with username and password fields', async ({ page }) => {
29 | await assertions.assertLoginFormVisible(page);
30 | });
31 |
32 | test('should reject empty login credentials', async ({ page }) => {
33 | const submitButton = page.locator('[data-testid="login-submit"]');
34 | await submitButton.click();
35 |
36 | // Should show validation error
37 | const errorMessage = page.locator('[data-testid="toast-error"]');
38 | await expect(errorMessage).toBeVisible({ timeout: 3000 });
39 | });
40 |
41 | test('should reject invalid LDAP credentials', async ({ page }) => {
42 | await auth.loginWithLdap(page, LDAP_USERS.invalid, BASE_URL);
43 |
44 | // Should show error message
45 | const errorToast = page.locator('[data-testid="toast-error"]');
46 | await expect(errorToast).toBeVisible({ timeout: 5000 });
47 |
48 | // Should still be on login page
49 | await expect(page).toHaveURL(BASE_URL);
50 | });
51 |
52 | test('should successfully login with valid LDAP credentials', async ({ page }) => {
53 | // Skip if LDAP is not configured in test environment
54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment');
55 |
56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL);
57 |
58 | // Verify we're authenticated
59 | await assertions.assertUserAuthenticated(page, BASE_URL);
60 |
61 | // Verify token is stored
62 | const token = await auth.getAuthToken(page);
63 | expect(token).toBeTruthy();
64 | });
65 |
66 | test('should reject invalid local user credentials', async ({ page }) => {
67 | // Switch to local user tab
68 | const localTab = page.locator('[data-testid="local-login-tab"]');
69 | await localTab.click();
70 |
71 | // Try to login with invalid credentials
> 72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user');
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password');
74 | await page.click('[data-testid="local-login-submit"]');
75 |
76 | // Should show error
77 | const errorToast = page.locator('[data-testid="toast-error"]');
78 | await expect(errorToast).toBeVisible({ timeout: 5000 });
79 | });
80 |
81 | test('should successfully login with valid local user credentials', async ({ page }) => {
82 | // Switch to local user tab
83 | const localTab = page.locator('[data-testid="local-login-tab"]');
84 | await localTab.click();
85 |
86 | // Login with valid credentials
87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
88 |
89 | // Verify authenticated
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
```

View File

@@ -1,187 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid LDAP credentials
- Location: e2e/workflows/1-login.spec.ts:41:7
# Error details
```
TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
Call log:
- waiting for locator('[data-testid="login-form"]') to be visible
- waiting for" http://localhost:8917/login" navigation to finish...
- navigated to "http://localhost:8917/login"
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
| ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
```

View File

@@ -1,213 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should display login form with username and password fields
- Location: e2e/workflows/1-login.spec.ts:28:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: locator('[data-testid="login-form"]')
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for locator('[data-testid="login-form"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { expect, Page, Locator } from '@playwright/test';
2 |
3 | /**
4 | * Custom Playwright assertions for E2E tests
5 | * Provides domain-specific matchers for inventory operations
6 | */
7 |
8 | /**
9 | * Assert that an inventory item is visible with expected data
10 | */
11 | export async function assertItemVisible(
12 | page: Page,
13 | itemName: string,
14 | expectedData?: { quantity?: number; category?: string }
15 | ): Promise<void> {
16 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${itemName}"]`);
17 | await expect(itemRow).toBeVisible();
18 |
19 | if (expectedData?.quantity !== undefined) {
20 | const quantityCell = itemRow.locator('[data-testid="quantity-cell"]');
21 | await expect(quantityCell).toContainText(expectedData.quantity.toString());
22 | }
23 |
24 | if (expectedData?.category !== undefined) {
25 | const categoryCell = itemRow.locator('[data-testid="category-cell"]');
26 | await expect(categoryCell).toContainText(expectedData.category);
27 | }
28 | }
29 |
30 | /**
31 | * Assert that a barcode scan was successful
32 | */
33 | export async function assertScanSuccess(
34 | page: Page,
35 | expectedItemName: string,
36 | expectedQuantityChange: number
37 | ): Promise<void> {
38 | // Check for success toast notification
39 | const successToast = page.locator('[data-testid="toast-success"]');
40 | await expect(successToast).toBeVisible({ timeout: 3000 });
41 |
42 | // Verify item quantity was updated
43 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${expectedItemName}"]`);
44 | await expect(itemRow).toBeVisible();
45 | }
46 |
47 | /**
48 | * Assert that login form is displayed
49 | */
50 | export async function assertLoginFormVisible(page: Page): Promise<void> {
51 | const loginForm = page.locator('[data-testid="login-form"]');
> 52 | await expect(loginForm).toBeVisible();
| ^ Error: expect(locator).toBeVisible() failed
53 |
54 | const usernameInput = page.locator('[data-testid="username-input"]');
55 | const passwordInput = page.locator('[data-testid="password-input"]');
56 | const submitButton = page.locator('[data-testid="login-submit"]');
57 |
58 | await expect(usernameInput).toBeVisible();
59 | await expect(passwordInput).toBeVisible();
60 | await expect(submitButton).toBeVisible();
61 | }
62 |
63 | /**
64 | * Assert that user is authenticated (on inventory page)
65 | */
66 | export async function assertUserAuthenticated(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
67 | // Check URL is on an authenticated page
68 | await expect(page).toHaveURL(/inventory|dashboard|scanner/);
69 |
70 | // Verify logout button is visible
71 | const logoutButton = page.locator('[data-testid="logout-button"]');
72 | await expect(logoutButton).toBeVisible();
73 | }
74 |
75 | /**
76 | * Assert that admin dashboard is visible
77 | */
78 | export async function assertAdminDashboardVisible(page: Page): Promise<void> {
79 | const adminOverlay = page.locator('[data-testid="admin-overlay"]');
80 | await expect(adminOverlay).toBeVisible();
81 |
82 | // Check for tabs
83 | const identityTab = page.locator('[data-testid="admin-tab-identity"]');
84 | const databaseTab = page.locator('[data-testid="admin-tab-database"]');
85 |
86 | await expect(identityTab).toBeVisible();
87 | await expect(databaseTab).toBeVisible();
88 | }
89 |
90 | /**
91 | * Assert that stock adjustment form is visible
92 | */
93 | export async function assertStockAdjustmentVisible(page: Page, itemName: string): Promise<void> {
94 | const adjustmentForm = page.locator('[data-testid="stock-adjustment-form"]');
95 | await expect(adjustmentForm).toBeVisible();
96 |
97 | const itemNameDisplay = page.locator(`[data-testid="adjustment-item-name"]`);
98 | await expect(itemNameDisplay).toContainText(itemName);
99 |
100 | const quantityInput = page.locator('[data-testid="adjustment-quantity-input"]');
101 | await expect(quantityInput).toBeVisible();
102 | }
103 |
104 | /**
105 | * Assert that AI extraction is in progress
106 | */
107 | export async function assertAiExtractionInProgress(page: Page): Promise<void> {
108 | const extractionOverlay = page.locator('[data-testid="ai-extraction-overlay"]');
109 | await expect(extractionOverlay).toBeVisible();
110 |
111 | const loadingSpinner = page.locator('[data-testid="extraction-loading"]');
112 | await expect(loadingSpinner).toBeVisible();
113 | }
114 |
115 | /**
116 | * Assert that AI extraction results are displayed
117 | */
118 | export async function assertAiExtractionResults(
119 | page: Page,
120 | expectedFields: { name?: string; partNumber?: string; quantity?: string }
121 | ): Promise<void> {
122 | const resultsForm = page.locator('[data-testid="extraction-results-form"]');
123 | await expect(resultsForm).toBeVisible();
124 |
125 | if (expectedFields.name) {
126 | const nameInput = resultsForm.locator('[data-testid="extracted-name"]');
127 | await expect(nameInput).toHaveValue(expectedFields.name);
128 | }
129 |
130 | if (expectedFields.partNumber) {
131 | const pnInput = resultsForm.locator('[data-testid="extracted-part-number"]');
132 | await expect(pnInput).toHaveValue(expectedFields.partNumber);
133 | }
134 |
135 | if (expectedFields.quantity) {
136 | const quantityInput = resultsForm.locator('[data-testid="extracted-quantity"]');
137 | await expect(quantityInput).toHaveValue(expectedFields.quantity);
138 | }
139 | }
140 |
141 | /**
142 | * Assert that offline sync is pending
143 | */
144 | export async function assertOfflineSyncPending(page: Page): Promise<void> {
145 | const syncIndicator = page.locator('[data-testid="offline-sync-indicator"]');
146 | await expect(syncIndicator).toBeVisible();
147 |
148 | const pendingBadge = page.locator('[data-testid="sync-pending-badge"]');
149 | await expect(pendingBadge).toBeVisible();
150 | }
151 |
152 | /**
```

View File

@@ -1,238 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should allow switching between LDAP and local login tabs
- Location: e2e/workflows/1-login.spec.ts:146:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: locator('[data-testid="ldap-login-form"]')
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for locator('[data-testid="ldap-login-form"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
52 | test('should successfully login with valid LDAP credentials', async ({ page }) => {
53 | // Skip if LDAP is not configured in test environment
54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment');
55 |
56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL);
57 |
58 | // Verify we're authenticated
59 | await assertions.assertUserAuthenticated(page, BASE_URL);
60 |
61 | // Verify token is stored
62 | const token = await auth.getAuthToken(page);
63 | expect(token).toBeTruthy();
64 | });
65 |
66 | test('should reject invalid local user credentials', async ({ page }) => {
67 | // Switch to local user tab
68 | const localTab = page.locator('[data-testid="local-login-tab"]');
69 | await localTab.click();
70 |
71 | // Try to login with invalid credentials
72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user');
73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password');
74 | await page.click('[data-testid="local-login-submit"]');
75 |
76 | // Should show error
77 | const errorToast = page.locator('[data-testid="toast-error"]');
78 | await expect(errorToast).toBeVisible({ timeout: 5000 });
79 | });
80 |
81 | test('should successfully login with valid local user credentials', async ({ page }) => {
82 | // Switch to local user tab
83 | const localTab = page.locator('[data-testid="local-login-tab"]');
84 | await localTab.click();
85 |
86 | // Login with valid credentials
87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
88 |
89 | // Verify authenticated
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
> 152 | await expect(ldapForm).toBeVisible();
| ^ Error: expect(locator).toBeVisible() failed
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
190 | await usernameInput.fill(LOCAL_USERS.admin.username);
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
212 | expect(await userItems.count()).toBeGreaterThan(0);
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
221 | await firstUser.click();
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should show user identity after successful login
- Location: e2e/workflows/1-login.spec.ts:93:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,197 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should handle network errors gracefully
- Location: e2e/workflows/1-login.spec.ts:181:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: locator.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
> 190 | await usernameInput.fill(LOCAL_USERS.admin.username);
| ^ Error: locator.fill: Test timeout of 30000ms exceeded.
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
212 | expect(await userItems.count()).toBeGreaterThan(0);
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
221 | await firstUser.click();
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should maintain session across page reloads
- Location: e2e/workflows/1-login.spec.ts:105:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,187 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid LDAP credentials
- Location: e2e/workflows/1-login.spec.ts:52:7
# Error details
```
TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
Call log:
- waiting for locator('[data-testid="login-form"]') to be visible
- waiting for" http://localhost:8917/login" navigation to finish...
- navigated to "http://localhost:8917/login"
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
| ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should show admin button for admin users
- Location: e2e/workflows/1-login.spec.ts:137:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,174 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should display user list in local login tab
- Location: e2e/workflows/1-login.spec.ts:202:7
# Error details
```
Error: expect(received).toBeGreaterThan(expected)
Expected: > 0
Received: 0
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e12]:
- generic [ref=e13]:
- button [ref=e14] [cursor=pointer]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: Logging in as
- paragraph [ref=e20]: Manual Input
- generic [ref=e21]:
- text: Username
- generic [ref=e22]:
- img [ref=e23]
- textbox "Admin" [ref=e26]
- generic [ref=e27]:
- text: Password
- generic [ref=e28]:
- img [ref=e29]
- textbox "Enter password" [active] [ref=e32]
- button "Verify Identity" [ref=e33] [cursor=pointer]
- button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]:
- img [ref=e40]
- alert [ref=e43]
```
# Test source
```ts
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
190 | await usernameInput.fill(LOCAL_USERS.admin.username);
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
> 212 | expect(await userItems.count()).toBeGreaterThan(0);
| ^ Error: expect(received).toBeGreaterThan(expected)
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
221 | await firstUser.click();
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

View File

@@ -1,169 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should auto-login when clicking user from list
- Location: e2e/workflows/1-login.spec.ts:215:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: locator.click: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="user-list-item"]').first()
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e12]:
- generic [ref=e13]:
- button [ref=e14] [cursor=pointer]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: Logging in as
- paragraph [ref=e20]: Manual Input
- generic [ref=e21]:
- text: Username
- generic [ref=e22]:
- img [ref=e23]
- textbox "Admin" [ref=e26]
- generic [ref=e27]:
- text: Password
- generic [ref=e28]:
- img [ref=e29]
- textbox "Enter password" [active] [ref=e32]
- button "Verify Identity" [ref=e33] [cursor=pointer]
- button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]:
- img [ref=e40]
- alert [ref=e43]
```
# Test source
```ts
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
190 | await usernameInput.fill(LOCAL_USERS.admin.username);
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
212 | expect(await userItems.count()).toBeGreaterThan(0);
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
> 221 | await firstUser.click();
| ^ Error: locator.click: Test timeout of 30000ms exceeded.
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

File diff suppressed because one or more lines are too long

View File

@@ -10,18 +10,11 @@ export default defineConfig({
timeout: 30000,
expect: { timeout: 5000 },
use: {
baseURL: 'http://localhost:8917',
baseURL: 'http://localhost',
trace: 'on-first-retry',
screenshot: 'only-on-failure',
},
webServer: {
command: 'NEXT_PUBLIC_API_URL=http://localhost:8916 npm run dev -- --port 8917',
url: 'http://localhost:8917',
reuseExistingServer: true,
timeout: 120000,
},
projects: [
{
name: 'chromium',

File diff suppressed because one or more lines are too long

View File

@@ -1,20 +0,0 @@
{
"status": "failed",
"failedTests": [
"c505212a9f19102e22e9-b8f769e1bec2e9dccfc0",
"c505212a9f19102e22e9-6a9f032de2702afdb378",
"c505212a9f19102e22e9-787cf157cb53bc071666",
"c505212a9f19102e22e9-84c2788f57def6af5ddb",
"c505212a9f19102e22e9-e95d7ac175608d44215d",
"c505212a9f19102e22e9-a0fc187a3d252408623f",
"c505212a9f19102e22e9-84172ac693ecaace19da",
"c505212a9f19102e22e9-61cc541ea57322557a4f",
"c505212a9f19102e22e9-1d3ef84183762c93c2f1",
"c505212a9f19102e22e9-c50a33e8ac1194b1cf3f",
"c505212a9f19102e22e9-416328d87be35eb14c6f",
"c505212a9f19102e22e9-f6e73b7d908d39f8af63",
"c505212a9f19102e22e9-2933a06b064cdb673ab5",
"c505212a9f19102e22e9-e264b575fec6c229ca47",
"c505212a9f19102e22e9-e34a858da752dac8c229"
]
}

View File

@@ -1,197 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should handle network errors gracefully
- Location: e2e/workflows/1-login.spec.ts:181:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: locator.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
> 190 | await usernameInput.fill(LOCAL_USERS.admin.username);
| ^ Error: locator.fill: Test timeout of 30000ms exceeded.
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
212 | expect(await userItems.count()).toBeGreaterThan(0);
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
221 | await firstUser.click();
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

View File

@@ -1,238 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should allow switching between LDAP and local login tabs
- Location: e2e/workflows/1-login.spec.ts:146:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: locator('[data-testid="ldap-login-form"]')
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for locator('[data-testid="ldap-login-form"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
52 | test('should successfully login with valid LDAP credentials', async ({ page }) => {
53 | // Skip if LDAP is not configured in test environment
54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment');
55 |
56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL);
57 |
58 | // Verify we're authenticated
59 | await assertions.assertUserAuthenticated(page, BASE_URL);
60 |
61 | // Verify token is stored
62 | const token = await auth.getAuthToken(page);
63 | expect(token).toBeTruthy();
64 | });
65 |
66 | test('should reject invalid local user credentials', async ({ page }) => {
67 | // Switch to local user tab
68 | const localTab = page.locator('[data-testid="local-login-tab"]');
69 | await localTab.click();
70 |
71 | // Try to login with invalid credentials
72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user');
73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password');
74 | await page.click('[data-testid="local-login-submit"]');
75 |
76 | // Should show error
77 | const errorToast = page.locator('[data-testid="toast-error"]');
78 | await expect(errorToast).toBeVisible({ timeout: 5000 });
79 | });
80 |
81 | test('should successfully login with valid local user credentials', async ({ page }) => {
82 | // Switch to local user tab
83 | const localTab = page.locator('[data-testid="local-login-tab"]');
84 | await localTab.click();
85 |
86 | // Login with valid credentials
87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
88 |
89 | // Verify authenticated
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
> 152 | await expect(ldapForm).toBeVisible();
| ^ Error: expect(locator).toBeVisible() failed
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
190 | await usernameInput.fill(LOCAL_USERS.admin.username);
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
212 | expect(await userItems.count()).toBeGreaterThan(0);
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
221 | await firstUser.click();
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

View File

@@ -1,169 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should auto-login when clicking user from list
- Location: e2e/workflows/1-login.spec.ts:215:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: locator.click: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="user-list-item"]').first()
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e12]:
- generic [ref=e13]:
- button [ref=e14] [cursor=pointer]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: Logging in as
- paragraph [ref=e20]: Manual Input
- generic [ref=e21]:
- text: Username
- generic [ref=e22]:
- img [ref=e23]
- textbox "Admin" [ref=e26]
- generic [ref=e27]:
- text: Password
- generic [ref=e28]:
- img [ref=e29]
- textbox "Enter password" [active] [ref=e32]
- button "Verify Identity" [ref=e33] [cursor=pointer]
- button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]:
- img [ref=e40]
- alert [ref=e43]
```
# Test source
```ts
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
190 | await usernameInput.fill(LOCAL_USERS.admin.username);
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
212 | expect(await userItems.count()).toBeGreaterThan(0);
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
> 221 | await firstUser.click();
| ^ Error: locator.click: Test timeout of 30000ms exceeded.
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid local user credentials
- Location: e2e/workflows/1-login.spec.ts:81:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,233 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid local user credentials
- Location: e2e/workflows/1-login.spec.ts:66:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e12]:
- generic [ref=e13]:
- button [ref=e14] [cursor=pointer]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: Logging in as
- paragraph [ref=e20]: Manual Input
- generic [ref=e21]:
- text: Username
- generic [ref=e22]:
- img [ref=e23]
- textbox "Admin" [ref=e26]
- generic [ref=e27]:
- text: Password
- generic [ref=e28]:
- img [ref=e29]
- textbox "Enter password" [active] [ref=e32]
- button "Verify Identity" [ref=e33] [cursor=pointer]
- button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]:
- img [ref=e40]
- alert [ref=e43]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 | import * as auth from '../fixtures/auth';
3 | import * as assertions from '../utils/assertions';
4 | import { LDAP_USERS, LOCAL_USERS } from '../fixtures/test-data';
5 |
6 | test.describe('Login Workflow', () => {
7 | const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
8 |
9 | test.beforeEach(async ({ page }) => {
10 | // Navigate to login page
11 | await page.goto(BASE_URL);
12 | // Wait for identity check overlay
13 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 10000 });
14 | });
15 |
16 | test('should display identity check overlay on app load', async ({ page }) => {
17 | const overlay = page.locator('[data-testid="identity-check-overlay"]');
18 | await expect(overlay).toBeVisible();
19 |
20 | // Verify tabs are available
21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
22 | const localTab = page.locator('[data-testid="local-login-tab"]');
23 |
24 | await expect(ldapTab).toBeVisible();
25 | await expect(localTab).toBeVisible();
26 | });
27 |
28 | test('should display login form with username and password fields', async ({ page }) => {
29 | await assertions.assertLoginFormVisible(page);
30 | });
31 |
32 | test('should reject empty login credentials', async ({ page }) => {
33 | const submitButton = page.locator('[data-testid="login-submit"]');
34 | await submitButton.click();
35 |
36 | // Should show validation error
37 | const errorMessage = page.locator('[data-testid="toast-error"]');
38 | await expect(errorMessage).toBeVisible({ timeout: 3000 });
39 | });
40 |
41 | test('should reject invalid LDAP credentials', async ({ page }) => {
42 | await auth.loginWithLdap(page, LDAP_USERS.invalid, BASE_URL);
43 |
44 | // Should show error message
45 | const errorToast = page.locator('[data-testid="toast-error"]');
46 | await expect(errorToast).toBeVisible({ timeout: 5000 });
47 |
48 | // Should still be on login page
49 | await expect(page).toHaveURL(BASE_URL);
50 | });
51 |
52 | test('should successfully login with valid LDAP credentials', async ({ page }) => {
53 | // Skip if LDAP is not configured in test environment
54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment');
55 |
56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL);
57 |
58 | // Verify we're authenticated
59 | await assertions.assertUserAuthenticated(page, BASE_URL);
60 |
61 | // Verify token is stored
62 | const token = await auth.getAuthToken(page);
63 | expect(token).toBeTruthy();
64 | });
65 |
66 | test('should reject invalid local user credentials', async ({ page }) => {
67 | // Switch to local user tab
68 | const localTab = page.locator('[data-testid="local-login-tab"]');
69 | await localTab.click();
70 |
71 | // Try to login with invalid credentials
> 72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user');
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password');
74 | await page.click('[data-testid="local-login-submit"]');
75 |
76 | // Should show error
77 | const errorToast = page.locator('[data-testid="toast-error"]');
78 | await expect(errorToast).toBeVisible({ timeout: 5000 });
79 | });
80 |
81 | test('should successfully login with valid local user credentials', async ({ page }) => {
82 | // Switch to local user tab
83 | const localTab = page.locator('[data-testid="local-login-tab"]');
84 | await localTab.click();
85 |
86 | // Login with valid credentials
87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
88 |
89 | // Verify authenticated
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
```

View File

@@ -1,187 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should successfully login with valid LDAP credentials
- Location: e2e/workflows/1-login.spec.ts:52:7
# Error details
```
TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
Call log:
- waiting for locator('[data-testid="login-form"]') to be visible
- waiting for" http://localhost:8917/login" navigation to finish...
- navigated to "http://localhost:8917/login"
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
| ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should maintain session across page reloads
- Location: e2e/workflows/1-login.spec.ts:105:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,213 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should display login form with username and password fields
- Location: e2e/workflows/1-login.spec.ts:28:7
# Error details
```
Error: expect(locator).toBeVisible() failed
Locator: locator('[data-testid="login-form"]')
Expected: visible
Timeout: 5000ms
Error: element(s) not found
Call log:
- Expect "toBeVisible" with timeout 5000ms
- waiting for locator('[data-testid="login-form"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { expect, Page, Locator } from '@playwright/test';
2 |
3 | /**
4 | * Custom Playwright assertions for E2E tests
5 | * Provides domain-specific matchers for inventory operations
6 | */
7 |
8 | /**
9 | * Assert that an inventory item is visible with expected data
10 | */
11 | export async function assertItemVisible(
12 | page: Page,
13 | itemName: string,
14 | expectedData?: { quantity?: number; category?: string }
15 | ): Promise<void> {
16 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${itemName}"]`);
17 | await expect(itemRow).toBeVisible();
18 |
19 | if (expectedData?.quantity !== undefined) {
20 | const quantityCell = itemRow.locator('[data-testid="quantity-cell"]');
21 | await expect(quantityCell).toContainText(expectedData.quantity.toString());
22 | }
23 |
24 | if (expectedData?.category !== undefined) {
25 | const categoryCell = itemRow.locator('[data-testid="category-cell"]');
26 | await expect(categoryCell).toContainText(expectedData.category);
27 | }
28 | }
29 |
30 | /**
31 | * Assert that a barcode scan was successful
32 | */
33 | export async function assertScanSuccess(
34 | page: Page,
35 | expectedItemName: string,
36 | expectedQuantityChange: number
37 | ): Promise<void> {
38 | // Check for success toast notification
39 | const successToast = page.locator('[data-testid="toast-success"]');
40 | await expect(successToast).toBeVisible({ timeout: 3000 });
41 |
42 | // Verify item quantity was updated
43 | const itemRow = page.locator(`[data-testid="item-row"][data-item-name="${expectedItemName}"]`);
44 | await expect(itemRow).toBeVisible();
45 | }
46 |
47 | /**
48 | * Assert that login form is displayed
49 | */
50 | export async function assertLoginFormVisible(page: Page): Promise<void> {
51 | const loginForm = page.locator('[data-testid="login-form"]');
> 52 | await expect(loginForm).toBeVisible();
| ^ Error: expect(locator).toBeVisible() failed
53 |
54 | const usernameInput = page.locator('[data-testid="username-input"]');
55 | const passwordInput = page.locator('[data-testid="password-input"]');
56 | const submitButton = page.locator('[data-testid="login-submit"]');
57 |
58 | await expect(usernameInput).toBeVisible();
59 | await expect(passwordInput).toBeVisible();
60 | await expect(submitButton).toBeVisible();
61 | }
62 |
63 | /**
64 | * Assert that user is authenticated (on inventory page)
65 | */
66 | export async function assertUserAuthenticated(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
67 | // Check URL is on an authenticated page
68 | await expect(page).toHaveURL(/inventory|dashboard|scanner/);
69 |
70 | // Verify logout button is visible
71 | const logoutButton = page.locator('[data-testid="logout-button"]');
72 | await expect(logoutButton).toBeVisible();
73 | }
74 |
75 | /**
76 | * Assert that admin dashboard is visible
77 | */
78 | export async function assertAdminDashboardVisible(page: Page): Promise<void> {
79 | const adminOverlay = page.locator('[data-testid="admin-overlay"]');
80 | await expect(adminOverlay).toBeVisible();
81 |
82 | // Check for tabs
83 | const identityTab = page.locator('[data-testid="admin-tab-identity"]');
84 | const databaseTab = page.locator('[data-testid="admin-tab-database"]');
85 |
86 | await expect(identityTab).toBeVisible();
87 | await expect(databaseTab).toBeVisible();
88 | }
89 |
90 | /**
91 | * Assert that stock adjustment form is visible
92 | */
93 | export async function assertStockAdjustmentVisible(page: Page, itemName: string): Promise<void> {
94 | const adjustmentForm = page.locator('[data-testid="stock-adjustment-form"]');
95 | await expect(adjustmentForm).toBeVisible();
96 |
97 | const itemNameDisplay = page.locator(`[data-testid="adjustment-item-name"]`);
98 | await expect(itemNameDisplay).toContainText(itemName);
99 |
100 | const quantityInput = page.locator('[data-testid="adjustment-quantity-input"]');
101 | await expect(quantityInput).toBeVisible();
102 | }
103 |
104 | /**
105 | * Assert that AI extraction is in progress
106 | */
107 | export async function assertAiExtractionInProgress(page: Page): Promise<void> {
108 | const extractionOverlay = page.locator('[data-testid="ai-extraction-overlay"]');
109 | await expect(extractionOverlay).toBeVisible();
110 |
111 | const loadingSpinner = page.locator('[data-testid="extraction-loading"]');
112 | await expect(loadingSpinner).toBeVisible();
113 | }
114 |
115 | /**
116 | * Assert that AI extraction results are displayed
117 | */
118 | export async function assertAiExtractionResults(
119 | page: Page,
120 | expectedFields: { name?: string; partNumber?: string; quantity?: string }
121 | ): Promise<void> {
122 | const resultsForm = page.locator('[data-testid="extraction-results-form"]');
123 | await expect(resultsForm).toBeVisible();
124 |
125 | if (expectedFields.name) {
126 | const nameInput = resultsForm.locator('[data-testid="extracted-name"]');
127 | await expect(nameInput).toHaveValue(expectedFields.name);
128 | }
129 |
130 | if (expectedFields.partNumber) {
131 | const pnInput = resultsForm.locator('[data-testid="extracted-part-number"]');
132 | await expect(pnInput).toHaveValue(expectedFields.partNumber);
133 | }
134 |
135 | if (expectedFields.quantity) {
136 | const quantityInput = resultsForm.locator('[data-testid="extracted-quantity"]');
137 | await expect(quantityInput).toHaveValue(expectedFields.quantity);
138 | }
139 | }
140 |
141 | /**
142 | * Assert that offline sync is pending
143 | */
144 | export async function assertOfflineSyncPending(page: Page): Promise<void> {
145 | const syncIndicator = page.locator('[data-testid="offline-sync-indicator"]');
146 | await expect(syncIndicator).toBeVisible();
147 |
148 | const pendingBadge = page.locator('[data-testid="sync-pending-badge"]');
149 | await expect(pendingBadge).toBeVisible();
150 | }
151 |
152 | /**
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should show admin button for admin users
- Location: e2e/workflows/1-login.spec.ts:137:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should show user identity after successful login
- Location: e2e/workflows/1-login.spec.ts:93:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should remember login preference on revisit
- Location: e2e/workflows/1-login.spec.ts:165:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -1,187 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should reject invalid LDAP credentials
- Location: e2e/workflows/1-login.spec.ts:41:7
# Error details
```
TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
Call log:
- waiting for locator('[data-testid="login-form"]') to be visible
- waiting for" http://localhost:8917/login" navigation to finish...
- navigated to "http://localhost:8917/login"
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
> 31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
| ^ TimeoutError: page.waitForSelector: Timeout 5000ms exceeded.
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
```

View File

@@ -1,174 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should display user list in local login tab
- Location: e2e/workflows/1-login.spec.ts:202:7
# Error details
```
Error: expect(received).toBeGreaterThan(expected)
Expected: > 0
Received: 0
```
# Page snapshot
```yaml
- generic [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e12]:
- generic [ref=e13]:
- button [ref=e14] [cursor=pointer]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: Logging in as
- paragraph [ref=e20]: Manual Input
- generic [ref=e21]:
- text: Username
- generic [ref=e22]:
- img [ref=e23]
- textbox "Admin" [ref=e26]
- generic [ref=e27]:
- text: Password
- generic [ref=e28]:
- img [ref=e29]
- textbox "Enter password" [active] [ref=e32]
- button "Verify Identity" [ref=e33] [cursor=pointer]
- button "Open Next.js Dev Tools" [ref=e39] [cursor=pointer]:
- img [ref=e40]
- alert [ref=e43]
```
# Test source
```ts
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
135 | });
136 |
137 | test('should show admin button for admin users', async ({ page }) => {
138 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
139 | await assertions.assertUserAuthenticated(page, BASE_URL);
140 |
141 | // Admin button should be visible
142 | const adminButton = page.locator('[data-testid="admin-button"]');
143 | await expect(adminButton).toBeVisible();
144 | });
145 |
146 | test('should allow switching between LDAP and local login tabs', async ({ page }) => {
147 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
148 | const localTab = page.locator('[data-testid="local-login-tab"]');
149 |
150 | // Start on LDAP tab
151 | let ldapForm = page.locator('[data-testid="ldap-login-form"]');
152 | await expect(ldapForm).toBeVisible();
153 |
154 | // Switch to local
155 | await localTab.click();
156 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
157 | await expect(ldapForm).not.toBeVisible();
158 |
159 | // Switch back to LDAP
160 | await ldapTab.click();
161 | ldapForm = page.locator('[data-testid="ldap-login-form"]');
162 | await expect(ldapForm).toBeVisible();
163 | });
164 |
165 | test('should remember login preference on revisit', async ({ page, context }) => {
166 | // Login with local user
167 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
168 |
169 | // Close and reopen browser
170 | await page.close();
171 | const newPage = await context.newPage();
172 | await newPage.goto(BASE_URL);
173 |
174 | // Should still be authenticated
175 | const authenticated = await auth.isAuthenticated(newPage);
176 | expect(authenticated).toBe(true);
177 |
178 | await newPage.close();
179 | });
180 |
181 | test('should handle network errors gracefully', async ({ page }) => {
182 | // Enable offline mode
183 | await page.context().setOffline(true);
184 |
185 | // Try to login
186 | const usernameInput = page.locator('[data-testid="username-input"]');
187 | const passwordInput = page.locator('[data-testid="password-input"]');
188 | const submitButton = page.locator('[data-testid="login-submit"]');
189 |
190 | await usernameInput.fill(LOCAL_USERS.admin.username);
191 | await passwordInput.fill(LOCAL_USERS.admin.password);
192 | await submitButton.click();
193 |
194 | // Should show network error
195 | const errorToast = page.locator('[data-testid="toast-error"]');
196 | await expect(errorToast).toBeVisible({ timeout: 5000 });
197 |
198 | // Re-enable network
199 | await page.context().setOffline(false);
200 | });
201 |
202 | test('should display user list in local login tab', async ({ page }) => {
203 | const localTab = page.locator('[data-testid="local-login-tab"]');
204 | await localTab.click();
205 |
206 | // User list should be visible
207 | const userList = page.locator('[data-testid="user-list"]');
208 | await expect(userList).toBeVisible();
209 |
210 | // Should have at least one user
211 | const userItems = page.locator('[data-testid="user-list-item"]');
> 212 | expect(await userItems.count()).toBeGreaterThan(0);
| ^ Error: expect(received).toBeGreaterThan(expected)
213 | });
214 |
215 | test('should auto-login when clicking user from list', async ({ page }) => {
216 | const localTab = page.locator('[data-testid="local-login-tab"]');
217 | await localTab.click();
218 |
219 | // Click first user in list
220 | const firstUser = page.locator('[data-testid="user-list-item"]').first();
221 | await firstUser.click();
222 |
223 | // Password input should be focused
224 | const passwordInput = page.locator('[data-testid="local-password-input"]');
225 | await expect(passwordInput).toBeFocused();
226 | });
227 | });
228 |
```

View File

@@ -1,192 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should reject empty login credentials
- Location: e2e/workflows/1-login.spec.ts:32:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: locator.click: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="login-submit"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { test, expect } from '@playwright/test';
2 | import * as auth from '../fixtures/auth';
3 | import * as assertions from '../utils/assertions';
4 | import { LDAP_USERS, LOCAL_USERS } from '../fixtures/test-data';
5 |
6 | test.describe('Login Workflow', () => {
7 | const BASE_URL = process.env.BASE_URL || 'http://localhost:8917';
8 |
9 | test.beforeEach(async ({ page }) => {
10 | // Navigate to login page
11 | await page.goto(BASE_URL);
12 | // Wait for identity check overlay
13 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 10000 });
14 | });
15 |
16 | test('should display identity check overlay on app load', async ({ page }) => {
17 | const overlay = page.locator('[data-testid="identity-check-overlay"]');
18 | await expect(overlay).toBeVisible();
19 |
20 | // Verify tabs are available
21 | const ldapTab = page.locator('[data-testid="ldap-login-tab"]');
22 | const localTab = page.locator('[data-testid="local-login-tab"]');
23 |
24 | await expect(ldapTab).toBeVisible();
25 | await expect(localTab).toBeVisible();
26 | });
27 |
28 | test('should display login form with username and password fields', async ({ page }) => {
29 | await assertions.assertLoginFormVisible(page);
30 | });
31 |
32 | test('should reject empty login credentials', async ({ page }) => {
33 | const submitButton = page.locator('[data-testid="login-submit"]');
> 34 | await submitButton.click();
| ^ Error: locator.click: Test timeout of 30000ms exceeded.
35 |
36 | // Should show validation error
37 | const errorMessage = page.locator('[data-testid="toast-error"]');
38 | await expect(errorMessage).toBeVisible({ timeout: 3000 });
39 | });
40 |
41 | test('should reject invalid LDAP credentials', async ({ page }) => {
42 | await auth.loginWithLdap(page, LDAP_USERS.invalid, BASE_URL);
43 |
44 | // Should show error message
45 | const errorToast = page.locator('[data-testid="toast-error"]');
46 | await expect(errorToast).toBeVisible({ timeout: 5000 });
47 |
48 | // Should still be on login page
49 | await expect(page).toHaveURL(BASE_URL);
50 | });
51 |
52 | test('should successfully login with valid LDAP credentials', async ({ page }) => {
53 | // Skip if LDAP is not configured in test environment
54 | test.skip(process.env.SKIP_LDAP === 'true', 'LDAP not available in test environment');
55 |
56 | await auth.loginWithLdap(page, LDAP_USERS.valid, BASE_URL);
57 |
58 | // Verify we're authenticated
59 | await assertions.assertUserAuthenticated(page, BASE_URL);
60 |
61 | // Verify token is stored
62 | const token = await auth.getAuthToken(page);
63 | expect(token).toBeTruthy();
64 | });
65 |
66 | test('should reject invalid local user credentials', async ({ page }) => {
67 | // Switch to local user tab
68 | const localTab = page.locator('[data-testid="local-login-tab"]');
69 | await localTab.click();
70 |
71 | // Try to login with invalid credentials
72 | await page.fill('[data-testid="local-username-input"]', 'invalid_user');
73 | await page.fill('[data-testid="local-password-input"]', 'wrong_password');
74 | await page.click('[data-testid="local-login-submit"]');
75 |
76 | // Should show error
77 | const errorToast = page.locator('[data-testid="toast-error"]');
78 | await expect(errorToast).toBeVisible({ timeout: 5000 });
79 | });
80 |
81 | test('should successfully login with valid local user credentials', async ({ page }) => {
82 | // Switch to local user tab
83 | const localTab = page.locator('[data-testid="local-login-tab"]');
84 | await localTab.click();
85 |
86 | // Login with valid credentials
87 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
88 |
89 | // Verify authenticated
90 | await assertions.assertUserAuthenticated(page, BASE_URL);
91 | });
92 |
93 | test('should show user identity after successful login', async ({ page }) => {
94 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
95 |
96 | // Check that user info is displayed
97 | const userDisplay = page.locator('[data-testid="user-display"]');
98 | await expect(userDisplay).toBeVisible();
99 |
100 | // Verify username is shown
101 | const username = await userDisplay.textContent();
102 | expect(username).toContain(LOCAL_USERS.admin.username);
103 | });
104 |
105 | test('should maintain session across page reloads', async ({ page }) => {
106 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
107 | await assertions.assertUserAuthenticated(page, BASE_URL);
108 |
109 | // Reload page
110 | await page.reload();
111 |
112 | // Should still be authenticated
113 | const token = await auth.getAuthToken(page);
114 | expect(token).toBeTruthy();
115 |
116 | // Should not be on login page
117 | await expect(page).not.toHaveURL(BASE_URL);
118 | });
119 |
120 | test('should support logout functionality', async ({ page }) => {
121 | // Login first
122 | await auth.loginWithLocalUser(page, LOCAL_USERS.admin, BASE_URL);
123 | await assertions.assertUserAuthenticated(page, BASE_URL);
124 |
125 | // Logout
126 | await auth.logout(page, BASE_URL);
127 |
128 | // Should be back on login page
129 | const identityOverlay = page.locator('[data-testid="identity-check-overlay"]');
130 | await expect(identityOverlay).toBeVisible({ timeout: 5000 });
131 |
132 | // Token should be cleared
133 | const token = await auth.getAuthToken(page);
134 | expect(token).toBeFalsy();
```

View File

@@ -1,224 +0,0 @@
# Instructions
- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.
# Test info
- Name: 1-login.spec.ts >> Login Workflow >> should support logout functionality
- Location: e2e/workflows/1-login.spec.ts:120:7
# Error details
```
Test timeout of 30000ms exceeded.
```
```
Error: page.fill: Test timeout of 30000ms exceeded.
Call log:
- waiting for locator('[data-testid="local-username-input"]')
```
# Page snapshot
```yaml
- generic [active] [ref=e1]:
- generic [ref=e3]:
- generic [ref=e4]:
- img [ref=e6]
- heading "Identity Check" [level=2] [ref=e9]
- paragraph [ref=e10]: Select operator profile or use direct login
- generic [ref=e11]:
- button "newuser user" [ref=e12] [cursor=pointer]:
- generic [ref=e13]:
- img [ref=e15]
- generic [ref=e18]:
- paragraph [ref=e19]: newuser
- paragraph [ref=e20]: user
- img [ref=e21]
- generic [ref=e23]:
- button "Enterprise" [ref=e24] [cursor=pointer]:
- img [ref=e25]
- text: Enterprise
- button "Manual Login" [ref=e28] [cursor=pointer]:
- img [ref=e29]
- text: Manual Login
- button "Open Next.js Dev Tools" [ref=e37] [cursor=pointer]:
- img [ref=e38]
- alert [ref=e41]
```
# Test source
```ts
1 | import { Page, BrowserContext } from '@playwright/test';
2 |
3 | /**
4 | * Authentication fixture for E2E tests
5 | * Handles login flows and session management
6 | */
7 |
8 | export interface LoginCredentials {
9 | username: string;
10 | password: string;
11 | }
12 |
13 | export interface AuthSession {
14 | token: string;
15 | userId: string;
16 | email: string;
17 | isAdmin: boolean;
18 | }
19 |
20 | /**
21 | * Perform LDAP authentication
22 | */
23 | export async function loginWithLdap(
24 | page: Page,
25 | credentials: LoginCredentials,
26 | baseUrl: string = 'http://localhost'
27 | ): Promise<void> {
28 | await page.goto(`${baseUrl}`);
29 |
30 | // Wait for login form to appear
31 | await page.waitForSelector('[data-testid="login-form"]', { timeout: 5000 });
32 |
33 | // Fill in username
34 | await page.fill('[data-testid="username-input"]', credentials.username);
35 |
36 | // Fill in password
37 | await page.fill('[data-testid="password-input"]', credentials.password);
38 |
39 | // Click login button
40 | await page.click('[data-testid="login-submit"]');
41 |
42 | // Wait for navigation to inventory page
43 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
44 | }
45 |
46 | /**
47 | * Perform local user authentication
48 | */
49 | export async function loginWithLocalUser(
50 | page: Page,
51 | credentials: LoginCredentials,
52 | baseUrl: string = 'http://localhost'
53 | ): Promise<void> {
54 | await page.goto(`${baseUrl}`);
55 |
56 | // Wait for identity check overlay
57 | await page.waitForSelector('[data-testid="identity-check-overlay"]', { timeout: 5000 });
58 |
59 | // Click "Local User" tab if it exists
60 | const localUserTab = page.locator('[data-testid="local-user-tab"]');
61 | if (await localUserTab.isVisible()) {
62 | await localUserTab.click();
63 | }
64 |
65 | // Fill in username
> 66 | await page.fill('[data-testid="local-username-input"]', credentials.username);
| ^ Error: page.fill: Test timeout of 30000ms exceeded.
67 |
68 | // Fill in password
69 | await page.fill('[data-testid="local-password-input"]', credentials.password);
70 |
71 | // Click login button
72 | await page.click('[data-testid="local-login-submit"]');
73 |
74 | // Wait for navigation to inventory page
75 | await page.waitForURL(`${baseUrl}/*`, { waitUntil: 'networkidle' });
76 | }
77 |
78 | /**
79 | * Logout from the application
80 | */
81 | export async function logout(page: Page, baseUrl: string = 'http://localhost'): Promise<void> {
82 | // Click logout button or menu
83 | const logoutButton = page.locator('[data-testid="logout-button"]');
84 | if (await logoutButton.isVisible()) {
85 | await logoutButton.click();
86 | } else {
87 | // Try finding it in menu
88 | const menu = page.locator('[data-testid="user-menu"]');
89 | if (await menu.isVisible()) {
90 | await menu.click();
91 | await page.click('[data-testid="logout-menu-item"]');
92 | }
93 | }
94 |
95 | // Wait for redirect to login
96 | await page.waitForURL(`${baseUrl}/*`, { timeout: 5000 });
97 | }
98 |
99 | /**
100 | * Get authentication token from local storage
101 | */
102 | export async function getAuthToken(page: Page): Promise<string | null> {
103 | const token = await page.evaluate(() => {
104 | return localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token');
105 | });
106 | return token;
107 | }
108 |
109 | /**
110 | * Set authentication token in local storage
111 | */
112 | export async function setAuthToken(page: Page, token: string): Promise<void> {
113 | await page.evaluate((t) => {
114 | localStorage.setItem('auth_token', t);
115 | }, token);
116 | }
117 |
118 | /**
119 | * Clear authentication data
120 | */
121 | export async function clearAuth(page: Page): Promise<void> {
122 | await page.evaluate(() => {
123 | localStorage.removeItem('auth_token');
124 | sessionStorage.removeItem('auth_token');
125 | localStorage.removeItem('user_data');
126 | sessionStorage.removeItem('user_data');
127 | });
128 | }
129 |
130 | /**
131 | * Check if user is authenticated
132 | */
133 | export async function isAuthenticated(page: Page): Promise<boolean> {
134 | const token = await getAuthToken(page);
135 | return !!token;
136 | }
137 |
138 | /**
139 | * Get current user info from page
140 | */
141 | export async function getCurrentUser(page: Page): Promise<{ username: string; email: string } | null> {
142 | try {
143 | const userData = await page.evaluate(() => {
144 | const stored = localStorage.getItem('user_data') || sessionStorage.getItem('user_data');
145 | return stored ? JSON.parse(stored) : null;
146 | });
147 | return userData;
148 | } catch {
149 | return null;
150 | }
151 | }
152 |
153 | /**
154 | * Wait for authentication to complete
155 | */
156 | export async function waitForAuth(page: Page, timeout: number = 5000): Promise<void> {
157 | // Wait for either:
158 | // 1. Token to be in storage
159 | // 2. Navigation to happen
160 | // 3. Auth overlay to disappear
161 | await page.waitForFunction(
162 | () => {
163 | const token =
164 | typeof window !== 'undefined'
165 | ? localStorage.getItem('auth_token') || sessionStorage.getItem('auth_token')
166 | : null;
```

View File

@@ -13,7 +13,6 @@ export default defineConfig({
globals: true,
environment: 'jsdom',
setupFiles: ['./tests/setup.ts'],
exclude: ['node_modules/**', 'e2e/**', '.next/**'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],