# Table Explorer Test Hints

## Overview
Comprehensive guide for testing Table Explorer functionality in Halo QA application, including navigation, filtering, sorting, pagination, property selection, and data validation.

## Critical Requirements

### Mandatory Test Configuration
```typescript
// ALWAYS include at top of test file
test.setTimeout(10 * 60 * 1000);  // 10 minutes for slow builds
```

### Required Imports
```typescript
import { test, expect } from '@playwright/test';
import { LoginPage } from '../../../../pages/login.page';
import { DashboardMainPage } from '../../../../pages/dashboard-main.page';
import * as dotenv from 'dotenv';

dotenv.config();
```

### Credentials Pattern
```typescript
// ALWAYS use environment variables with fallbacks
const testEmail = process.env.TEST_EMAIL || 'cdp@hy.ly';
const testPassword = process.env.TEST_PASSWORD || 'BMy2L1rRXZlkKoTU7';
```

## Navigation Patterns

### Opening Table Explorer Dialog
```typescript
// CRITICAL: Try multiple selector approaches in priority order
const tableExplorerRadio = page.getByRole('radio', { name: 'Table Explorer' });
const tableExplorerButton = page.getByRole('button', { name: 'Table Explorer' });
const tableExplorerText = page.getByText('Table Explorer', { exact: true });

// Check visibility
const radioVisible = await tableExplorerRadio.isVisible().catch(() => false);
const buttonVisible = await tableExplorerButton.isVisible().catch(() => false);
const textVisible = await tableExplorerText.isVisible().catch(() => false);

// Click based on what's visible
if (buttonVisible) {
  await tableExplorerButton.click();
} else if (radioVisible) {
  await tableExplorerRadio.click();
} else if (textVisible) {
  await tableExplorerText.click();
} else {
  throw new Error('Table Explorer not found in any form!');
}
```

### Expanding Categories and Selecting Tables
```typescript
// PATTERN: Use exact text matching
const occupancyTrends = page.getByText('Occupancy Trends', { exact: true });
await occupancyTrends.click();
await page.waitForTimeout(2000);

const byPropertiesTable = page.getByText('By Properties', { exact: true });
await byPropertiesTable.click();
await page.waitForTimeout(2000);
```

### Closing Table Explorer Dialog
```typescript
// PATTERN: Click outside dialog to close
await page.mouse.click(100, 100);
await page.waitForTimeout(3000);
```

## Dynamic Column Detection Pattern

### CRITICAL: Always Use Dynamic Column Detection
**NEVER hardcode column names** - tables have different columns depending on data source.

```typescript
// ✅ CORRECT: Dynamic column detection
const headerCells = page.locator('thead th');
const headerCount = await headerCells.count();
const columnNames: string[] = [];

for (let i = 0; i < headerCount; i++) {
  const headerText = await headerCells.nth(i).textContent();
  columnNames.push(headerText?.trim() || '');
}

// Primary column is the first non-empty, non-filter column
const primaryColumn = columnNames.find(h =>
  h &&
  h.trim() !== '' &&
  !h.includes('>=') &&
  !h.startsWith('>')
) || '';

// Data columns exclude primary and filter columns
const dataColumns = columnNames.filter(h =>
  h &&
  h !== primaryColumn &&
  !h.includes('>=') &&
  !h.startsWith('>') &&
  h.trim() !== ''
);

console.log(`📊 Detected columns: Primary="${primaryColumn}", Data=[${dataColumns.join(', ')}]`);
```

```typescript
// ❌ WRONG: Hardcoded column names
const isPrimaryColumn = h.includes('Property Name') || h.includes('Campaign Name');
const isDataColumn = h.includes('Sessions') || h.includes('Revenue');
```

### Column Detection Logic
- **Primary Column**: First non-empty column that's NOT a filter column (doesn't contain '>=' or start with '>')
- **Data Columns**: All other non-empty columns excluding primary and filter columns
- **Filter Columns**: Columns containing '>=' or starting with '>' (these are filter controls, not data)

## Strict Assertions Pattern

### MANDATORY: All Assertions Must Have Error Messages
```typescript
// ✅ CORRECT: Assertion with descriptive error message
expect(rowCount, `❌ TABLE DATA FAILED: No data rows found in "By Properties" table. Expected at least 1 row, got ${rowCount}`).toBeGreaterThan(0);

expect(headerCount, `❌ TABLE STRUCTURE FAILED: No table headers found for "By Properties" table. Expected at least 1 header, got ${headerCount}`).toBeGreaterThan(0);

expect(filename, `❌ FILENAME VALIDATION FAILED: Downloaded file "${filename}" does not contain "Property Breakdown"`).toContain('Property Breakdown');

expect(firstRowBackToFirst?.trim(), `❌ FIRST BUTTON FAILED: After clicking First button, expected to return to first row "${firstRowBefore?.trim()}", but got "${firstRowBackToFirst?.trim()}"`).toBe(firstRowBefore?.trim());
```

```typescript
// ❌ WRONG: Assertion without error message
expect(rowCount).toBeGreaterThan(0);
expect(headerCount).toBeGreaterThan(0);
```

### Error Message Format
```typescript
`❌ [ERROR_TYPE]: [Description]. Expected [X], Got [Y]`
```

Examples:
- `❌ TABLE DATA FAILED: ...`
- `❌ TABLE STRUCTURE FAILED: ...`
- `❌ SORTING FAILED: ...`
- `❌ FILTER FAILED: ...`
- `❌ PAGINATION FAILED: ...`
- `❌ DROPDOWN FAILED: ...`

## Sorting Tests Pattern

### Dynamic Sorting Implementation
```typescript
await test.step('Test dynamic sorting on data columns', async () => {
  for (const columnName of dataColumns) {
    console.log(`\n📊 Testing sorting on column: "${columnName}"`);

    // Find and click column header
    const columnHeader = page.locator(`thead th:has-text("${columnName}")`).first();
    const headerVisible = await columnHeader.isVisible({ timeout: 5000 }).catch(() => false);

    if (!headerVisible) {
      console.log(`⚠️ Column "${columnName}" not found - skipping`);
      continue;
    }

    // Click to sort ascending
    await columnHeader.click();
    await page.waitForTimeout(2000);

    // Get first 3 values
    const firstCell = await page.locator(`tbody tr:nth-child(1) td:nth-child(${dataColumns.indexOf(columnName) + 2})`).textContent();
    const secondCell = await page.locator(`tbody tr:nth-child(2) td:nth-child(${dataColumns.indexOf(columnName) + 2})`).textContent();

    console.log(`   Ascending: First="${firstCell?.trim()}", Second="${secondCell?.trim()}"`);

    // Click to sort descending
    await columnHeader.click();
    await page.waitForTimeout(2000);

    const firstCellDesc = await page.locator(`tbody tr:nth-child(1) td:nth-child(${dataColumns.indexOf(columnName) + 2})`).textContent();

    console.log(`   Descending: First="${firstCellDesc?.trim()}"`);
    expect(firstCellDesc?.trim()).not.toBe(firstCell?.trim());
  }
});
```

## Filtering Tests Pattern

### Dynamic Filter Detection and Testing
```typescript
await test.step('Test dynamic filtering on data columns', async () => {
  for (const columnName of dataColumns) {
    console.log(`\n🔍 Testing filter on column: "${columnName}"`);

    const filterColumn = columnNames.find(h => h.includes(`>=${columnName}`) || h.startsWith(`>${columnName}`));

    if (!filterColumn) {
      console.log(`   No filter control for "${columnName}" - skipping`);
      continue;
    }

    // Find filter input
    const filterInput = page.locator(`input[placeholder*="${columnName}"], input[aria-label*="${columnName}"]`).first();
    const inputVisible = await filterInput.isVisible({ timeout: 3000 }).catch(() => false);

    if (!inputVisible) {
      console.log(`   Filter input not found for "${columnName}"`);
      continue;
    }

    // Apply filter
    await filterInput.fill('100');
    await page.waitForTimeout(2000);

    const rowsAfterFilter = await page.locator('tbody tr').count();
    console.log(`   After filter: ${rowsAfterFilter} rows`);

    // Clear filter
    await filterInput.clear();
    await page.waitForTimeout(2000);
  }
});
```

## Pagination Pattern

### Testing Pagination Controls
```typescript
await test.step('Test pagination navigation with all buttons', async () => {
  // Get pagination SVG buttons (4 buttons: <<, <, >, >>)
  const paginationSvgs = page.locator('svg.hyly-gc-text-custom-3');
  const firstButton = paginationSvgs.nth(0);  // <<
  const prevButton = paginationSvgs.nth(1);   // <
  const nextButton = paginationSvgs.nth(2);   // >
  const lastButton = paginationSvgs.nth(3);   // >>

  // Test Next button
  await nextButton.click();
  await page.waitForTimeout(2000);

  const firstRowPage2 = await page.locator('tbody tr').first().locator('td').first().textContent();
  expect(firstRowPage2?.trim()).not.toBe(firstRowBefore?.trim());

  // Test Last button
  await lastButton.click();
  await page.waitForTimeout(2000);

  // Test Previous button
  await prevButton.click();
  await page.waitForTimeout(2000);

  // Test First button
  await firstButton.click();
  await page.waitForTimeout(2000);

  const firstRowBackToFirst = await page.locator('tbody tr').first().locator('td').first().textContent();
  expect(firstRowBackToFirst?.trim(), `❌ FIRST BUTTON FAILED: After clicking First button, expected to return to first row "${firstRowBefore?.trim()}", but got "${firstRowBackToFirst?.trim()}"`).toBe(firstRowBefore?.trim());
});
```

### Rows Per Page Dropdown
```typescript
await test.step('Verify rows per page dropdown options', async () => {
  const rowsSelector = page.locator('text=/\\d+ rows/').first();
  await rowsSelector.click();
  await page.waitForTimeout(1000);

  const dropdownOptions = page.locator('[role="listbox"] [role="option"], [role="menu"] [role="menuitem"], li:has-text("rows")');
  const optionCount = await dropdownOptions.count();

  expect(optionCount, `❌ DROPDOWN FAILED: Rows per page dropdown has ${optionCount} options. Expected at least 1 option.`).toBeGreaterThanOrEqual(1);

  // Select smallest option
  await dropdownOptions.first().click();
  await page.waitForTimeout(2000);
});
```

## Property Selection Pattern

### Testing Property Dropdown
```typescript
await test.step('Test property selection dropdown', async () => {
  // Find property dropdown
  const propertyDropdown = page.locator('.custom-dropdown-parent').filter({ hasText: 'Properties' }).first();

  // Get initial state
  const initialValue = await propertyDropdown.textContent();
  const rowsBefore = await page.locator('tbody tr').count();

  // Open dropdown
  await propertyDropdown.click();
  await page.waitForTimeout(2000);

  // Find checkboxes
  const propertyOptions = page.locator('li, [role="option"], [role="menuitem"]').filter({ hasText: /^(?!.*Select All).*/ });
  const optionCount = await propertyOptions.count();

  // Toggle a checkbox
  for (let i = 0; i < Math.min(10, optionCount); i++) {
    const option = propertyOptions.nth(i);
    const checkbox = option.locator('input[type="checkbox"]');
    const hasCheckbox = await checkbox.count() > 0;

    if (hasCheckbox) {
      const isChecked = await checkbox.isChecked();
      if (!isChecked) {
        await checkbox.click();
        break;
      }
    }
  }

  await page.waitForTimeout(2000);

  // Verify change
  const updatedValue = await propertyDropdown.textContent();
  expect(updatedValue?.trim()).not.toBe(initialValue?.trim());

  const rowsAfter = await page.locator('tbody tr').count();
  expect(rowsAfter, `❌ PROPERTY FILTER FAILED: After changing property selection, table has ${rowsAfter} rows. Expected at least 1 row.`).toBeGreaterThan(0);
});
```

## Wait Time Standards

### Required Wait Times
```typescript
// Initial page stabilization
await page.waitForTimeout(40000); // 40 seconds

// Before Table Explorer interaction
await page.waitForTimeout(30000); // 30 seconds

// After opening Table Explorer dialog
await page.waitForTimeout(3000); // 3 seconds

// After expanding category
await page.waitForTimeout(2000); // 2 seconds

// After selecting table
await page.waitForTimeout(2000); // 2 seconds

// After closing dialog
await page.waitForTimeout(3000); // 3 seconds

// After sort/filter actions
await page.waitForTimeout(2000); // 2 seconds
```

## Common Selectors

### Table Structure
```typescript
// Headers
const headerCells = page.locator('thead th');

// Rows
const tableRows = page.locator('tbody tr');

// Specific cell
const cell = page.locator(`tbody tr:nth-child(${row}) td:nth-child(${col})`);

// Pagination info
const paginationInfo = page.locator('text=/\\d+-\\d+ of \\d+/').first();

// Rows per page selector
const rowsSelector = page.locator('text=/\\d+ rows/').first();
```

### Export Controls
```typescript
// Export CSV button
const exportButton = page.locator('svg[data-tooltip-id="exportCSV-tooltip"]').first();

// Export link
const exportLink = page.locator('a[download*="Property Breakdown"]').first();
```

## Console Logging Pattern

### Informative Logging
```typescript
// Start of action
console.log('🔍 Looking for Table Explorer in sidebar...');

// Success
console.log('✅ Successfully logged in to Halo QA');
console.log('✅ Table Explorer dialog is open!');

// Data discovery
console.log(`📊 Found ${rowCount} table rows`);
console.log(`📊 Detected columns: Primary="${primaryColumn}", Data=[${dataColumns.join(', ')}]`);

// Actions
console.log('🔘 Clicked property dropdown');
console.log('📋 Strategy: Checking an unchecked property');

// Warnings
console.log('⚠️ Expected 4 pagination buttons, found ${svgCount}');
console.log('ℹ️ Property dropdown not available for this table - skipping selection test');
```

## Test Structure Template

```typescript
test('@table-explorer REG-XX: Test description', async ({ page }) => {
  // Initialize page objects
  const loginPage = new LoginPage(page);
  const dashboardPage = new DashboardMainPage(page);

  const testEmail = process.env.TEST_EMAIL || 'cdp@hy.ly';
  const testPassword = process.env.TEST_PASSWORD || 'BMy2L1rRXZlkKoTU7';

  // Step 1: Navigate and Login
  await test.step('Navigate and Login to Halo QA', async () => {
    await loginPage.goto();
    await loginPage.login(testEmail, testPassword);
    await dashboardPage.waitForDashboardLoad();
    console.log('✅ Successfully logged in to Halo QA');
  });

  // Step 2: Wait for page stabilization
  await test.step('Wait for page to fully load and stabilize', async () => {
    console.log('⏳ Waiting for page to fully load and stabilize...');
    await page.waitForTimeout(40000); // 40 seconds
    console.log('✅ Initial page load complete');
  });

  // Step 3: Open Table Explorer
  await test.step('Open Table Explorer dialog', async () => {
    // ... navigation code ...
  });

  // Step 4+: Test-specific steps
  // ...

  // Final: Verification
  await test.step('Verify test completed', async () => {
    console.log('✅ Test completed successfully');
    console.log('📋 Summary:');
    console.log('   ✅ Item 1');
    console.log('   ✅ Item 2');
  });
});
```

## Common Issues to Avoid

### ❌ NEVER Do This
```typescript
// Hardcoded column names
const isSessionsColumn = h.includes('Sessions');

// Assertions without error messages
expect(rowCount).toBeGreaterThan(0);

// Using waitForLoadState('networkidle')
await page.waitForLoadState('networkidle');

// Hardcoded credentials
await page.fill('input[type="email"]', 'cdp@hy.ly');

// Short wait times on slow builds
await page.waitForTimeout(1000); // Too short!
```

### ✅ ALWAYS Do This
```typescript
// Dynamic column detection
const primaryColumn = columnNames.find(h => h && h.trim() !== '' && !h.includes('>='));

// Strict assertions with error messages
expect(rowCount, `❌ TABLE DATA FAILED: Expected at least 1 row, got ${rowCount}`).toBeGreaterThan(0);

// Use waitForTimeout instead
await page.waitForTimeout(2000);

// Environment variables with fallbacks
const testEmail = process.env.TEST_EMAIL || 'cdp@hy.ly';

// Adequate wait times
await page.waitForTimeout(40000); // Initial load
```

## CSV Export Validation

### Complete Export Test Pattern
```typescript
await test.step('Click export CSV button and verify download', async () => {
  // Set up download listener
  const downloadPromise = page.waitForEvent('download', { timeout: 30000 });

  // Click export
  const exportButton = page.locator('svg[data-tooltip-id="exportCSV-tooltip"]').first();
  await exportButton.click();

  // Wait for download
  const download = await downloadPromise;
  const filename = download.suggestedFilename();

  // Validate filename
  expect(filename, `❌ FILENAME VALIDATION FAILED: Downloaded file "${filename}" does not contain expected text`).toContain('Property Breakdown');
  expect(filename.toLowerCase()).toMatch(/\.(csv|xlsx|xls)$/);

  // Save and verify
  const downloadPath = path.join(__dirname, '../../../../test-results', filename);
  await download.saveAs(downloadPath);

  // Check file size
  if (fs.existsSync(downloadPath)) {
    const stats = fs.statSync(downloadPath);
    expect(stats.size, `❌ CSV FILE EMPTY: Downloaded CSV file has ${stats.size} bytes. Expected file size > 0 bytes.`).toBeGreaterThan(0);

    // Verify CSV content
    const fileContent = fs.readFileSync(downloadPath, 'utf-8');
    const lines = fileContent.split('\n').slice(0, 3);

    expect(lines.length, `❌ CSV VALIDATION FAILED: CSV file has ${lines.length} lines. Expected at least 2 lines (header + data).`).toBeGreaterThan(1);

    // Cleanup
    fs.unlinkSync(downloadPath);
  }
});
```

## Summary Checklist

Before completing a table explorer test, verify:

- ✅ Test timeout set to 10 minutes
- ✅ Page objects imported and used correctly
- ✅ Environment variables used for credentials
- ✅ Dynamic column detection implemented (NO hardcoded column names)
- ✅ All assertions have descriptive error messages
- ✅ Proper wait times used (40s initial, 30s before Table Explorer, etc.)
- ✅ Multiple selector approaches for Table Explorer
- ✅ Console logging for all major actions and discoveries
- ✅ Test steps organized with test.step()
- ✅ Final summary logged at end of test
- ✅ Proper error handling with meaningful messages
