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
1 change: 1 addition & 0 deletions apps/e2e/scripts/run-all.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ const ALWAYS = [
'src/tests/sql-editor-peek-stacking.test.ts',
'src/tests/sql-editor-data-peek-grid.test.ts',
'src/tests/sql-editor-compare-multi-target.test.ts',
'src/tests/sql-editor-compare-window.test.ts',
'src/tests/sql-editor-join-typing.test.ts',
].join(' '),
label: 'SQL Editor',
Expand Down
36 changes: 36 additions & 0 deletions apps/e2e/src/generated/test-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2239,10 +2239,16 @@ export const TestIds = {
sqlResultCompareClose: (statementIndex: string | number) => `sql-result-compare-close-${statementIndex}`,
/** select */
sqlResultCompareDest: (statementIndex: string | number) => `sql-result-compare-dest-${statementIndex}`,
/** button · Difference › */
sqlResultCompareDiffNext: (statementIndex: string | number) => `sql-result-compare-diff-next-${statementIndex}`,
/** button · ‹ Difference */
sqlResultCompareDiffPrev: (statementIndex: string | number) => `sql-result-compare-diff-prev-${statementIndex}`,
/** button · Export all compare grids as one CSV (columns prefixed by Source / Target) */
sqlResultCompareExportCsv: (statementIndex: string | number) => `sql-result-compare-export-csv-${statementIndex}`,
/** button · Export all compare grids as one CSV */
sqlResultCompareExportCsvModal: (statementIndex: string | number) => `sql-result-compare-export-csv-modal-${statementIndex}`,
/** button */
sqlResultCompareFilter: (f: string | number, statementIndex: string | number) => `sql-result-compare-filter-${f}-${statementIndex}`,
/** p */
sqlResultCompareHint: (statementIndex: string | number) => `sql-result-compare-hint-${statementIndex}`,
/** span */
Expand All @@ -2253,6 +2259,14 @@ export const TestIds = {
sqlResultCompareMaximizedHint: (statementIndex: string | number) => `sql-result-compare-maximized-hint-${statementIndex}`,
/** div */
sqlResultCompareModal: (statementIndex: string | number) => `sql-result-compare-modal-${statementIndex}`,
/** span */
sqlResultComparePageLabel: (statementIndex: string | number) => `sql-result-compare-page-label-${statementIndex}`,
/** button · Next */
sqlResultComparePageNext: (statementIndex: string | number) => `sql-result-compare-page-next-${statementIndex}`,
/** button · Prev */
sqlResultComparePagePrev: (statementIndex: string | number) => `sql-result-compare-page-prev-${statementIndex}`,
/** select · Rows per compare page */
sqlResultComparePageSize: (statementIndex: string | number) => `sql-result-compare-page-size-${statementIndex}`,
/** input */
sqlResultCompareSkipTrigger: (statementIndex: string | number) => `sql-result-compare-skip-trigger-${statementIndex}`,
/** input */
Expand All @@ -2263,6 +2277,16 @@ export const TestIds = {
sqlResultCompareToggle: (statementIndex: string | number) => `sql-result-compare-toggle-${statementIndex}`,
/** div */
sqlResultCompareToolbar: (statementIndex: string | number) => `sql-result-compare-toolbar-${statementIndex}`,
/** div */
sqlResultCompareView: (statementIndex: string | number) => `sql-result-compare-view-${statementIndex}`,
/** button · Back to loaded rows */
sqlResultCompareWindowClear: (statementIndex: string | number) => `sql-result-compare-window-clear-${statementIndex}`,
/** span */
sqlResultCompareWindowError: (statementIndex: string | number) => `sql-result-compare-window-error-${statementIndex}`,
/** button · Compare rows */
sqlResultCompareWindowRun: (statementIndex: string | number) => `sql-result-compare-window-run-${statementIndex}`,
/** span */
sqlResultCompareWindowSummary: (statementIndex: string | number) => `sql-result-compare-window-summary-${statementIndex}`,
/** section */
sqlResultCred: (connectionId: string | number) => `sql-result-cred-${connectionId}`,
/** button · Close maximized results */
Expand Down Expand Up @@ -4823,18 +4847,30 @@ export type TestId =
| `sql-result-compare-baseline-${string}`
| `sql-result-compare-close-${string}`
| `sql-result-compare-dest-${string}`
| `sql-result-compare-diff-next-${string}`
| `sql-result-compare-diff-prev-${string}`
| `sql-result-compare-export-csv-${string}`
| `sql-result-compare-export-csv-modal-${string}`
| `sql-result-compare-filter-${string}-${string}`
| `sql-result-compare-hint-${string}`
| `sql-result-compare-legend-${string}`
| `sql-result-compare-maximize-${string}`
| `sql-result-compare-maximized-hint-${string}`
| `sql-result-compare-modal-${string}`
| `sql-result-compare-page-label-${string}`
| `sql-result-compare-page-next-${string}`
| `sql-result-compare-page-prev-${string}`
| `sql-result-compare-page-size-${string}`
| `sql-result-compare-skip-trigger-${string}`
| `sql-result-compare-sync-scroll-${string}`
| `sql-result-compare-sync-scroll-modal-${string}`
| `sql-result-compare-toggle-${string}`
| `sql-result-compare-toolbar-${string}`
| `sql-result-compare-view-${string}`
| `sql-result-compare-window-clear-${string}`
| `sql-result-compare-window-error-${string}`
| `sql-result-compare-window-run-${string}`
| `sql-result-compare-window-summary-${string}`
| `sql-result-cred-${string}`
| `sql-result-cred-close-${string}`
| `sql-result-cred-maximize-${string}`
Expand Down
162 changes: 162 additions & 0 deletions apps/e2e/src/tests/sql-editor-compare-window.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
/**
* Compare 2,000 rows: both sides read again in key order, every row inside the
* window classified, and the result paged and filtered in the browser.
*
* The grids page each side on its own with OFFSET, so their pages do not line
* up and a one-sided row on a cut-short side can only be "unresolved". A
* compare run reads the first 2,000 rows of both sides by key instead.
*
* Fixture (every count below follows from it):
* source ids 1–2,500, name n<id>
* target the same, without 7, 1,500 and 2,300; names changed at 10, 20 and
* 1,990; plus 2,600 and 2,601
* In the window, source ends at id 2,000 and target at 2,002, so:
* Changed 3 · Only in source 2 (7, 1,500) · Only in target 0 ·
* Unresolved 2 (2,001 and 2,002, past the source's last key) ·
* Matching 1,995 · All 2,002
*
* Two SQLite files under a per-run directory. Requires the web app + API and
* a `sqlite3` CLI on PATH.
*/
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { execFileSync, execSync } from 'node:child_process';
import { mkdirSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import type { Page } from 'playwright';
import { buildDriver, quitDriver } from '../helpers/driver.js';
import { saveScreenshot } from '../helpers/screenshot.js';
import { byTestId } from '../helpers/test-ids.js';
import { AppPage } from '../pages/AppPage.js';
import { SqlEditorPage } from '../pages/SqlEditorPage.js';

const RUN = Date.now().toString(36);
const DIR = `/tmp/foxschema-e2e-compare-window-${RUN}`;
const SOURCE = { file: join(DIR, 'source.db'), name: `E2E Window Src ${RUN}` };
const TARGET = { file: join(DIR, 'target.db'), name: `E2E Window Tgt ${RUN}` };

function hasSqlite3(): boolean {
try {
execSync('which sqlite3', { stdio: 'ignore' });
return true;
} catch {
return false;
}
}

function seed(path: string, target: boolean): void {
const missing = new Set([7, 1500, 2300]);
const changed = new Set([10, 20, 1990]);
const rows: string[] = [];
for (let id = 1; id <= 2500; id++) {
if (target && missing.has(id)) continue;
rows.push(`(${id}, '${target && changed.has(id) ? `changed${id}` : `n${id}`}')`);
}
if (target) rows.push("(2600, 'n2600')", "(2601, 'n2601')");
execFileSync('sqlite3', [path], {
input: `CREATE TABLE items (id INTEGER PRIMARY KEY, name TEXT NOT NULL);
INSERT INTO items (id, name) VALUES ${rows.join(',')};`,
});
}

describe.skipIf(!hasSqlite3())('SQL Editor · Compare 2,000 rows', () => {
let driver: Page;
let sql: SqlEditorPage;
const label = (id: string) => driver.locator(byTestId(id as never)).innerText();

beforeAll(async () => {
rmSync(DIR, { recursive: true, force: true });
mkdirSync(DIR, { recursive: true });
seed(SOURCE.file, false);
seed(TARGET.file, true);

driver = await buildDriver();
sql = new SqlEditorPage(driver);
await new AppPage(driver).open();
await sql.resetPersistedEditorState();
await driver.reload();
await driver.waitForSelector(byTestId('toolbar'));
await sql.addSqliteCredential(SOURCE.name, SOURCE.file);
await sql.addSqliteCredential(TARGET.name, TARGET.file);
}, 180_000);

afterAll(async () => {
if (driver) await quitDriver(driver);
rmSync(DIR, { recursive: true, force: true });
});

it('compares the first 2,000 rows of both sides by key, classifying every row the window can decide', async () => {
await sql.openView();
await sql.checkConnection(SOURCE.name);
await sql.checkConnection(TARGET.name);
await sql.setSql('SELECT id, name FROM items;');
await sql.run();
await sql.waitForResults();
await sql.setLayoutSideBySide();
await sql.compareToggle(0).click();

// The source is the first destination checked; the target, the other.
const baseline = await driver.locator(byTestId('sql-result-compare-baseline-0')).inputValue();
const sourceValue = await driver.evaluate(
(name) =>
[...(document.querySelector('[data-testid="sql-result-compare-baseline-0"]') as HTMLSelectElement).options].find(
(o) => o.textContent?.includes(name)
)?.value,
SOURCE.name
);
if (sourceValue && sourceValue !== baseline) {
await driver.locator(byTestId('sql-result-compare-baseline-0')).selectOption(sourceValue);
}

await driver.locator(byTestId('sql-result-compare-window-run-0')).click();
await driver.waitForSelector(byTestId('sql-result-compare-window-summary-0'), { timeout: 60_000 });
expect(await label('sql-result-compare-window-summary-0')).toMatch(
/Compared 2,000 \(more follow\) source and 2,000 \(more follow\) target rows, ordered by id/
);

const count = async (f: string) => {
const text = await label(`sql-result-compare-filter-${f}-0`);
return Number(text.replace(/[^\d]/g, ''));
};
expect({
changed: await count('changed'),
source: await count('source'),
target: await count('target'),
unresolved: await count('unresolved'),
matching: await count('matching'),
all: await count('all'),
}).toEqual({ changed: 3, source: 2, target: 0, unresolved: 2, matching: 1995, all: 2002 });
await saveScreenshot(driver, 'compare-window-summary');
}, 180_000);

it('filters to the changed rows and pages the rest in the browser', async () => {
await driver.locator(byTestId('sql-result-compare-filter-changed-0')).click();
expect(await label('sql-result-compare-page-label-0')).toBe('page 1 of 1');
const keys = await driver.evaluate(() =>
[...document.querySelectorAll('[data-testid="sql-data-grid"]')][0]!
.querySelectorAll('tbody tr [data-testid="sql-row-num"]').length
);
expect(keys).toBe(3);

await driver.locator(byTestId('sql-result-compare-filter-all-0')).click();
await driver.locator(byTestId('sql-result-compare-page-size-0')).selectOption('100');
expect(await label('sql-result-compare-page-label-0')).toBe('page 1 of 21');
}, 60_000);

it('steps from one difference to the next across pages of matching rows', async () => {
// Differences sit at ids 7, 10 and 20 (page 1), 1,500 (page 15) and 1,990 (page 20).
await driver.locator(byTestId('sql-result-compare-diff-next-0')).click();
expect(await label('sql-result-compare-page-label-0')).toBe('page 15 of 21');
await driver.locator(byTestId('sql-result-compare-diff-next-0')).click();
expect(await label('sql-result-compare-page-label-0')).toBe('page 20 of 21');
expect(await driver.locator(byTestId('sql-result-compare-diff-next-0')).isDisabled()).toBe(true);
await driver.locator(byTestId('sql-result-compare-diff-prev-0')).click();
expect(await label('sql-result-compare-page-label-0')).toBe('page 15 of 21');
await saveScreenshot(driver, 'compare-window-difference');
}, 60_000);

it('goes back to the loaded pages, where a cut-short side leaves one-sided rows unresolved', async () => {
await driver.locator(byTestId('sql-result-compare-window-clear-0')).click();
await driver.waitForSelector(byTestId('sql-result-compare-window-run-0'));
expect(await driver.locator(byTestId('sql-result-compare-window-summary-0')).count()).toBe(0);
}, 60_000);
});
Loading
Loading