This document outlines the comprehensive testing strategy for the ShipCES Micro-Widget Architecture, including unit tests, integration tests, and end-to-end tests using Playwright.
-
Test:
MicroWidgets.renderCompanyName()- ✓ Renders company name from
customer_namefield - ✓ Falls back to parsing
from_addressifcustomer_nameis null - ✓ Truncates long names to 20 characters
- ✓ Includes company icon emoji
- ✓ Renders company name from
-
Test:
MicroWidgets.renderTimestamp()- ✓ Formats timestamp as relative time ("2h ago", "15m ago", "3d ago")
- ✓ Shows full ISO timestamp in tooltip
- ✓ Updates correctly for different time ranges
-
Test:
MicroWidgets.renderFlag()- ✓ Shows filled flag (🚩) when
is_flaggedis true - ✓ Shows outline flag (⚐) when
is_flaggedis false - ✓ Displays correct tooltip with flagger name and time
- ✓ Includes onclick handler with correct parameters
- ✓ Shows filled flag (🚩) when
-
Test:
MicroWidgets.renderExpiryTimer()- ✓ Shows "Expired" for expired RFQs
- ✓ Calculates correct time remaining based on source
- ✓ Returns correct phase (normal/warning/urgent/critical/final/expired)
- ✓ Applies correct color for each phase
- ✓ Formats time as "Xh Ym" for hours and minutes
-
Test:
MicroWidgets.renderServiceType()- ✓ Renders correct icon for each service type
- ✓ Applies correct background color from constants
- ✓ Converts service type to uppercase for display
- ✓ Falls back to "Standard" for unknown service types
-
Test:
MicroWidgets.renderVehicleType()- ✓ Renders AI badge when recommendation exists
- ✓ Uses recommended vehicle over default vehicle type
- ✓ Applies color coding based on vehicle dimensions
- ✓ Includes formatted tooltip with capacity details
-
Test:
MicroWidgets.renderConfidence()- ✓ Returns empty div when no recommendation exists
- ✓ Shows correct color for high/medium/low confidence
- ✓ Displays percentage with % symbol
- ✓ Includes reasoning in tooltip
-
Test:
MicroWidgets.parseCustomerName()- ✓ Extracts name from "Name " format
- ✓ Returns email username if no name found
- ✓ Handles edge cases (no angle brackets, missing parts)
-
Test:
MicroWidgets.calculateTimeRemaining()- ✓ Returns correct remaining time for each source
- ✓ Calculates correct phase based on percentage
- ✓ Handles negative time (expired)
- ✓ Uses correct timer settings (Gmail: 4h, Outlook: 4h, Board: 24h)
-
Test:
MicroWidgets.formatTimeAgo()- ✓ Returns "Xm ago" for < 60 minutes
- ✓ Returns "Xh ago" for < 24 hours
- ✓ Returns "Xd ago" for >= 24 hours
-
Test:
MicroWidgets.estimateTravelTime()- ✓ Calculates travel time at 55 mph average
- ✓ Returns "X minutes" for < 1 hour
- ✓ Returns "X hours" for < 24 hours
- ✓ Returns "X days" for >= 24 hours
- Test: Main grid initialization
- ✓ Grid initializes with correct configuration
- ✓ Event handlers attach successfully
- ✓ renderCB function is defined before init
- Test: RFQ card with nested grids
- ✓ Parent RFQ card container renders
- ✓ Three nested row containers render (header, route, service)
- ✓ Each row contains correct number of micro-widgets
- ✓ Nested grids have correct column/cellHeight/margin settings
- Test: RFQ data propagation
- ✓ Parent widget passes
rfqDatato children - ✓ All micro-widgets receive correct RFQ object
- ✓ Widgets extract correct fields from RFQ data
- ✓ Missing optional fields don't break rendering
- ✓ Parent widget passes
- Test: Save/load layout
- ✓
grid.save(true, true)captures all nested widgets - ✓ Saved data includes rfqData for all widgets
- ✓ Loading restores exact layout configuration
- ✓ Nested grids maintain their structure after load
- ✓
test('Micro-widget demo page loads successfully', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await expect(page.locator('h1')).toContainText('ShipCES Micro-Widget Architecture');
await expect(page.locator('.grid-stack')).toBeVisible();
await expect(page.locator('button:has-text("Load RFQ Cards")')).toBeVisible();
});test('Load RFQ Cards button populates grid', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
// Wait for cards to render
await page.waitForSelector('.rfq-card-container', { timeout: 5000 });
// Verify correct number of cards loaded
const cards = await page.locator('.rfq-card-container').count();
expect(cards).toBe(5); // SAMPLE_RFQS has 5 entries
// Verify statistics updated
const totalRFQs = await page.locator('#totalRFQs').textContent();
expect(totalRFQs).toBe('5');
});test('Header row contains all 6 micro-widgets', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
// Verify each widget type exists
await expect(page.locator('.micro-widget.company-name').first()).toBeVisible();
await expect(page.locator('.micro-widget.timestamp').first()).toBeVisible();
await expect(page.locator('.micro-widget.flag').first()).toBeVisible();
await expect(page.locator('.micro-widget.move-icon').first()).toBeVisible();
await expect(page.locator('.micro-widget.feedback').first()).toBeVisible();
await expect(page.locator('.micro-widget.expiry').first()).toBeVisible();
});test('Route row contains origin, arrow, destination, distance', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
// Verify route widgets
await expect(page.locator('.micro-widget.location.origin').first()).toBeVisible();
await expect(page.locator('.micro-widget.arrow').first()).toBeVisible();
await expect(page.locator('.micro-widget.location.destination').first()).toBeVisible();
await expect(page.locator('.micro-widget.distance').first()).toBeVisible();
// Verify city names are displayed
const originCity = await page.locator('.micro-widget.location.origin .city').first().textContent();
expect(originCity).toBeTruthy();
});test('Service row contains service type, vehicle, special, confidence', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
// Verify service widgets
await expect(page.locator('.micro-widget.service-type').first()).toBeVisible();
await expect(page.locator('.micro-widget.vehicle-type').first()).toBeVisible();
// Note: special-instructions and confidence may be empty for some RFQs
const specialCount = await page.locator('.micro-widget.special-instructions:not(.empty)').count();
const confidenceCount = await page.locator('.micro-widget.confidence:not(.empty)').count();
console.log(`Special instructions visible: ${specialCount}`);
console.log(`Confidence badges visible: ${confidenceCount}`);
});test('Clicking flag widget toggles flagged state', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.micro-widget.flag');
// Find an unflagged widget
const unflaggedFlag = page.locator('.micro-widget.flag.unflagged').first();
await expect(unflaggedFlag).toBeVisible();
// Get initial flagged count
const initialCount = await page.locator('#flaggedCount').textContent();
// Click to flag
await unflaggedFlag.click();
// Verify it's now flagged
await expect(unflaggedFlag).toHaveClass(/flagged/);
// Verify count increased
const newCount = await page.locator('#flaggedCount').textContent();
expect(parseInt(newCount)).toBe(parseInt(initialCount) + 1);
// Click again to unflag
await unflaggedFlag.click();
// Verify it's unflagged again
await expect(unflaggedFlag).toHaveClass(/unflagged/);
});test('Clicking move icon shows dropdown menu', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.micro-widget.move-icon');
const moveIcon = page.locator('.micro-widget.move-icon').first();
const moveMenu = moveIcon.locator('.move-menu');
// Initially hidden
await expect(moveMenu).toBeHidden();
// Click to show
await moveIcon.click();
await expect(moveMenu).toBeVisible();
// Verify menu options exist
const options = await moveMenu.locator('.move-option').count();
expect(options).toBeGreaterThan(0);
// Verify workflow state options
await expect(moveMenu.locator('.move-option:has-text("Incomplete")')).toBeVisible();
await expect(moveMenu.locator('.move-option:has-text("Submitted")')).toBeVisible();
});test('Expiry timers display correct phase colors', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.micro-widget.expiry');
// Get all expiry widgets
const expiry Widgets = page.locator('.micro-widget.expiry');
const count = await expiryWidgets.count();
expect(count).toBeGreaterThan(0);
// Check for different phases
for (let i = 0; i < count; i++) {
const widget = expiryWidgets.nth(i);
const className = await widget.getAttribute('class');
// Should have a phase class
const hasPhase = className.includes('normal') ||
className.includes('warning') ||
className.includes('urgent') ||
className.includes('critical') ||
className.includes('final') ||
className.includes('expired');
expect(hasPhase).toBe(true);
// Should have background color
const bgColor = await widget.evaluate(el => window.getComputedStyle(el).backgroundColor);
expect(bgColor).toBeTruthy();
}
});test('Micro-widgets can be dragged within rows', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.micro-widget.company-name');
// Get initial position of company name widget
const companyWidget = page.locator('.micro-widget.company-name').first();
const initialBox = await companyWidget.boundingBox();
// Drag the widget (small drag to stay within row)
await companyWidget.hover();
await page.mouse.down();
await page.mouse.move(initialBox.x + 100, initialBox.y);
await page.mouse.up();
// Wait for animation
await page.waitForTimeout(500);
// Get new position
const newBox = await companyWidget.boundingBox();
// Position should have changed
expect(newBox.x).not.toBe(initialBox.x);
});test('Toggle button locks and unlocks micro-widgets', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.micro-widget.company-name');
const toggleButton = page.locator('button:has-text("Lock Micro-Widgets")');
await expect(toggleButton).toBeVisible();
// Click to lock
await toggleButton.click();
// Button text should change
await expect(toggleButton).toContainText('Unlock');
// Verify nested grids have locked class
const nestedGrids = page.locator('.grid-stack-nested.locked');
const lockedCount = await nestedGrids.count();
expect(lockedCount).toBeGreaterThan(0);
// Click to unlock
await toggleButton.click();
await expect(toggleButton).toContainText('Lock');
});test('Save layout persists to localStorage', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
// Save layout
page.on('dialog', dialog => dialog.accept());
await page.click('button:has-text("Save Layout")');
// Verify localStorage has data
const saved = await page.evaluate(() => localStorage.getItem('shipces-micro-layout'));
expect(saved).toBeTruthy();
const data = JSON.parse(saved);
expect(data.children).toBeDefined();
expect(data.children.length).toBeGreaterThan(0);
});
test('Load saved layout restores configuration', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
// First load and save
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
page.on('dialog', dialog => dialog.accept());
await page.click('button:has-text("Save Layout")');
// Clear grid
await page.click('button:has-text("Clear")');
await page.waitForTimeout(500);
// Verify grid is empty
const emptyCount = await page.locator('.rfq-card-container').count();
expect(emptyCount).toBe(0);
// Load saved layout
await page.click('button:has-text("Load Saved")');
await page.waitForSelector('.rfq-card-container');
// Verify cards restored
const restoredCount = await page.locator('.rfq-card-container').count();
expect(restoredCount).toBe(5);
});test('Spacing slider updates grid margin', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
const slider = page.locator('#spacingSlider');
const valueDisplay = page.locator('#spacingValue');
// Initial value should be 2
await expect(valueDisplay).toContainText('2');
// Move slider to 8
await slider.fill('8');
// Value should update
await expect(valueDisplay).toContainText('8');
// Grid margin should update (visual verification would require screenshot comparison)
});test('Statistics update correctly', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
// Initially zero
await expect(page.locator('#totalRFQs')).toContainText('0');
await expect(page.locator('#totalWidgets')).toContainText('0');
// Load RFQs
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
// Stats should update
const totalRFQs = await page.locator('#totalRFQs').textContent();
expect(parseInt(totalRFQs)).toBeGreaterThan(0);
const totalWidgets = await page.locator('#totalWidgets').textContent();
expect(parseInt(totalWidgets)).toBeGreaterThan(10); // Many nested widgets
// Add one more RFQ
await page.click('button:has-text("Add RFQ Card")');
await page.waitForTimeout(500);
const newTotal = await page.locator('#totalRFQs').textContent();
expect(parseInt(newTotal)).toBe(parseInt(totalRFQs) + 1);
});test('Micro-widget layout matches expected design', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
// Take screenshot of first RFQ card
const firstCard = page.locator('.rfq-card-container').first();
await expect(firstCard).toHaveScreenshot('rfq-card-baseline.png');
});test('Page loads and initializes within 3 seconds', async ({ page }) => {
const startTime = Date.now();
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
const loadTime = Date.now() - startTime;
expect(loadTime).toBeLessThan(3000);
console.log(`Page loaded in ${loadTime}ms`);
});test('Grid handles 10 RFQ cards without performance degradation', async ({ page }) => {
await page.goto('http://localhost:8080/demo/shipces-micro.html');
await page.click('button:has-text("Load RFQ Cards")');
await page.waitForSelector('.rfq-card-container');
// Add 5 more cards
for (let i = 0; i < 5; i++) {
await page.click('button:has-text("Add RFQ Card")');
await page.waitForTimeout(100);
}
// Verify 10 cards exist
const count = await page.locator('.rfq-card-container').count();
expect(count).toBe(10);
// Verify page is still responsive
const flag = page.locator('.micro-widget.flag').first();
await flag.click();
// Should respond within 500ms
});# Install Playwright
npm install -D @playwright/test
# Run all tests
npx playwright test
# Run specific test file
npx playwright test micro-widget.spec.js
# Run in headed mode (see browser)
npx playwright test --headed
# Run in debug mode
npx playwright test --debug
# Generate test report
npx playwright show-report- Unit Tests: 90%+ coverage of utility functions
- Integration Tests: All widget types rendered correctly
- E2E Tests: All user interactions work as expected
- Visual Tests: No unintended design regressions
- Performance: Page loads < 3s, interactions respond < 500ms
name: Micro-Widget Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm install
- run: npx playwright install --with-deps
- run: npx playwright test
- uses: actions/upload-artifact@v3
if: always()
with:
name: playwright-report
path: playwright-report/- Load RFQ Cards button populates grid
- All 5 sample RFQs render correctly
- Each RFQ has 14 micro-widgets (6 header + 4 route + 4 service)
- Company names display correctly
- Timestamps show relative time
- Flag widgets toggle on click
- Move menu opens on click and shows all workflow states
- Feedback widgets show count badge when > 0
- Expiry timers show correct phase colors
- Origin and destination cities display
- Distance shows in miles or "- mi"
- Service type badges have correct colors
- Vehicle type badges show AI indicator when recommended
- HAZMAT badge appears for hazardous shipments
- Confidence badges show percentage and color
- Micro-widgets can be dragged within rows
- Lock button disables micro-widget dragging
- Save layout persists to localStorage
- Load saved layout restores configuration
- Spacing slider adjusts margins
- Statistics update correctly
- Add RFQ Card button adds new card
- Clear button removes all widgets
- Timer updates every minute
- Responsive design works on mobile
- Timer Update Interval: Timers update every 60 seconds, so countdown may appear frozen between updates
- LocalStorage Limit: Very large layouts (100+ RFQ cards) may exceed localStorage quota
- Mobile Dragging: Touch interactions may need fine-tuning for small screens
- Nested Grid Depth: GridStack may have performance issues beyond 3 levels of nesting
- Accessibility tests (ARIA labels, keyboard navigation)
- Cross-browser compatibility tests (Safari, Firefox, Edge)
- Mobile device tests (iOS, Android)
- Load testing (1000+ widgets)
- Memory leak detection
- API integration tests (when backend connected)
- WebSocket real-time update tests
Last Updated: February 13, 2026 Test Framework: Playwright v1.40+ Coverage Tool: Istanbul/NYC