Skip to content

Latest commit

 

History

History
606 lines (480 loc) · 21 KB

File metadata and controls

606 lines (480 loc) · 21 KB

ShipCES Micro-Widget Testing Documentation

Test Plan Overview

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 Categories

1. Unit Tests

Widget Rendering Functions

  • Test: MicroWidgets.renderCompanyName()

    • ✓ Renders company name from customer_name field
    • ✓ Falls back to parsing from_address if customer_name is null
    • ✓ Truncates long names to 20 characters
    • ✓ Includes company icon emoji
  • 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_flagged is true
    • ✓ Shows outline flag (⚐) when is_flagged is false
    • ✓ Displays correct tooltip with flagger name and time
    • ✓ Includes onclick handler with correct parameters
  • 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

Utility Functions

  • 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

2. Integration Tests

Grid Initialization

  • Test: Main grid initialization
    • ✓ Grid initializes with correct configuration
    • ✓ Event handlers attach successfully
    • ✓ renderCB function is defined before init

Nested Grid Creation

  • 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

Widget Data Flow

  • Test: RFQ data propagation
    • ✓ Parent widget passes rfqData to children
    • ✓ All micro-widgets receive correct RFQ object
    • ✓ Widgets extract correct fields from RFQ data
    • ✓ Missing optional fields don't break rendering

Layout Serialization

  • 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

3. End-to-End Tests (Playwright)

Page Load and Initialization

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();
});

RFQ Card Loading

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');
});

Header Row Micro-Widgets

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();
});

Route Row Micro-Widgets

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();
});

Service Row Micro-Widgets

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}`);
});

Interactive: Flag Toggle

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/);
});

Interactive: Move Menu

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();
});

Interactive: Expiry Timer Updates

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();
  }
});

Layout: Micro-Widget Dragging

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);
});

Layout: Lock/Unlock Micro-Widgets

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');
});

Layout: Save and Load

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);
});

Responsive: Spacing Slider

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)
});

Statistics Updates

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);
});

4. Visual Regression Tests

Screenshot Comparison

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');
});

5. Performance Tests

Load Time

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`);
});

Many Widgets

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
});

Test Execution

Running Playwright Tests

# 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

Test Coverage Goals

  • 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

Continuous Integration

GitHub Actions Workflow

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/

Manual Testing Checklist

  • 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

Known Issues

  1. Timer Update Interval: Timers update every 60 seconds, so countdown may appear frozen between updates
  2. LocalStorage Limit: Very large layouts (100+ RFQ cards) may exceed localStorage quota
  3. Mobile Dragging: Touch interactions may need fine-tuning for small screens
  4. Nested Grid Depth: GridStack may have performance issues beyond 3 levels of nesting

Future Test Additions

  • 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