Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions apps/e2e/src/pages/SqlEditorPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,27 @@ export class SqlEditorPage {
await clickWhen(this.page, '[data-testid="sql-layout-by-credential"]');
}

/** Side-by-side Compare toolbar for statement index (default 0). */
compareToggle(statementIndex = 0) {
return this.page.locator(`[data-testid="sql-result-compare-toggle-${statementIndex}"]`);
}

compareLegend(statementIndex = 0) {
return this.page.locator(`[data-testid="sql-result-compare-legend-${statementIndex}"]`);
}

compareBaselineSelect(statementIndex = 0) {
return this.page.locator(`[data-testid="sql-result-compare-baseline-${statementIndex}"]`);
}

/** Count cells marked with a data-diff attribute in the results panel. */
async diffCellCount(kind?: 'modified' | 'missing' | 'extra'): Promise<number> {
const sel = kind
? `[data-testid="sql-results-side-by-side"] td[data-diff="${kind}"]`
: '[data-testid="sql-results-side-by-side"] td[data-diff]';
return this.page.locator(sel).count();
}

async tabCount(): Promise<number> {
return this.page.locator('[data-testid="sql-editor-tabs"] [role="tab"]').count();
}
Expand Down
50 changes: 50 additions & 0 deletions apps/e2e/src/tests/sql-editor-sqlite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { mkdirSync, rmSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import type { Page } from 'playwright';
import { buildDriver, quitDriver } from '../helpers/driver.js';
import { saveScreenshot, saveSeoScreenshot } from '../helpers/screenshot.js';
import { AppPage } from '../pages/AppPage.js';
import { SqlEditorPage } from '../pages/SqlEditorPage.js';

Expand Down Expand Up @@ -102,6 +103,55 @@ describe.skipIf(!ready)('SQL Editor · SQLite multi-credential', () => {
await sql.setLayoutByCredential();
});

it('compares seeded rows across credentials with colored cell diffs', async () => {
// Seeds: id=1 is Alice vs Bob (modified); id=2 Shared/Austin matches.
await sql.openView();
await sql.checkConnection(NAME_A);
await sql.checkConnection(NAME_B);
await sql.setSql('SELECT id, name, city FROM customers ORDER BY id;');
await sql.run();
await sql.waitForResults();
await sql.setLayoutSideBySide();

await driver.waitForSelector('[data-testid="sql-result-compare-toggle-0"]', {
timeout: 10_000,
});
expect(await sql.compareToggle(0).isChecked()).toBe(true);
expect(await sql.compareLegend(0).isVisible()).toBe(true);
expect(await sql.compareBaselineSelect(0).isVisible()).toBe(true);

// Wait for highlight pass after Compare defaults on.
await driver.waitForFunction(
() =>
document.querySelectorAll('[data-testid="sql-results-side-by-side"] td[data-diff]').length >
0,
{ timeout: 10_000 }
);

const modified = await sql.diffCellCount('modified');
const anyDiff = await sql.diffCellCount();
expect(modified).toBeGreaterThanOrEqual(2); // name cell tinted on both grids
expect(anyDiff).toBeGreaterThanOrEqual(modified);

const results = await sql.resultsText();
expect(results).toMatch(/baseline/i);
expect(results).toMatch(/differ|match/i);

// Capture the colored compare view for the PR / walkthrough.
await saveScreenshot(driver, 'sql-editor-data-compare');
await saveSeoScreenshot(driver, 'sql-editor-data-compare');

// Toggle Compare off → highlights clear.
await sql.compareToggle(0).click();
await driver.waitForFunction(
() =>
document.querySelectorAll('[data-testid="sql-results-side-by-side"] td[data-diff]')
.length === 0,
{ timeout: 10_000 }
);
expect(await sql.diffCellCount()).toBe(0);
});

it('shows the statement strip for multi-statement SQL', async () => {
await sql.setSql('SELECT 1 AS n;\nSELECT name FROM customers WHERE id = 1;');
expect(await sql.statementStripVisible()).toBe(true);
Expand Down
15 changes: 13 additions & 2 deletions apps/web/src/frontend/components/sql-editor/DataGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { AlertTriangle, Download, GripVertical, RefreshCw } from 'lucide-react';
import type { SqlStatementResult } from '../../api/sqlApi';
import { CELL_DIFF_CLASS, type CellDiffKind } from '../../lib/resultDataDiff';
Comment thread
github-code-quality[bot] marked this conversation as resolved.
Fixed
import { columnToListValues, rowsForTableVariable } from '../../lib/sql-variables';
import { useSqlEditorStore } from '../../store/useSqlEditorStore';
import { downloadCsv } from '../../utils/exportCsv';
Expand Down Expand Up @@ -268,6 +269,11 @@ export const DataGrid: React.FC<{
* compact grid so side-by-side compares stay dense.
*/
emphasis?: boolean;
/**
* Cross-connection data compare: return a highlight kind for a cell
* (`modified` / `missing` / `extra`), or null when unchanged.
*/
cellHighlight?: (rowIdx: number, colIdx: number) => CellDiffKind | null;
}> = React.memo(
({
result,
Expand All @@ -290,6 +296,7 @@ export const DataGrid: React.FC<{
onSelectRow,
toolbarExtra,
emphasis = false,
cellHighlight,
}) => {
const upsertVariable = useSqlEditorStore((s) => s.upsertVariable);
const rowH = emphasis ? ROW_H_EMPHASIS_PX : ROW_H_PX;
Expand Down Expand Up @@ -710,14 +717,18 @@ export const DataGrid: React.FC<{
const w = colWidths[colIdx] ?? COL_DEFAULT_PX;
const { text, title, isNull } = cellDisplay(cell);
const kind = isNull ? 'null' : (colKinds[colIdx] ?? 'string');
const hl = cellHighlight?.(i, colIdx) ?? null;
const hlClass = hl ? CELL_DIFF_CLASS[hl] : '';
const cellBg = hlClass || `${rowBg} group-hover:bg-[var(--fox-grid-bg-hover)]`;
return (
<td
key={colIdx}
className={`px-3 overflow-hidden text-ellipsis ${rowBg} group-hover:bg-[var(--fox-grid-bg-hover)] ${KIND_CELL_CLASS[kind]} ${
data-diff={hl ?? undefined}
className={`px-3 overflow-hidden text-ellipsis ${cellBg} ${KIND_CELL_CLASS[kind]} ${
emphasis && kind === 'string' ? 'font-semibold' : ''
} ${emphasis && kind === 'number' ? 'font-bold' : ''}`}
style={{ width: w, minWidth: COL_MIN_PX, maxWidth: w }}
title={title}
title={hl ? `${hl}: ${title}` : title}
onContextMenu={(e) => {
e.preventDefault();
setMenu({
Expand Down
Loading
Loading