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
57 changes: 35 additions & 22 deletions apps/e2e/src/pages/LokeeHistoryPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,35 +76,48 @@ export class LokeeHistoryPage {
await select.selectOption(value);
}

async clickTableNode(tableName: string): Promise<void> {
await this.clickObjectNamed(tableName);
/** Single owner of the object-node selector, shared by every accessor below. */
private objectNode(name: string) {
return this.page.locator('[data-testid^="rf-object-"]').filter({ hasText: name }).first();
}

async clickObjectNamed(name: string): Promise<void> {
const node = this.page
.locator('[data-testid^="rf-object-"]')
.filter({ hasText: name })
.first();
const node = this.objectNode(name);
await node.waitFor({ state: 'visible', timeout: 15_000 });
await node.click();
await waitFor(this.page, '[data-testid="lokee-object-inspector"]', 20_000);
}

async objectNamedVisible(name: string): Promise<boolean> {
return this.page
.locator('[data-testid^="rf-object-"]')
.filter({ hasText: name })
.first()
.isVisible()
try {
await node.click({ timeout: 5_000 });
} catch {
// React Flow clips its pane, so a node in a far-right column can sit
// outside the viewport. Fit the graph and click again for real — a
// dispatched synthetic click would bypass the actionability check, which
// is the one thing this test exists to prove a user can do.
await this.page.locator('.react-flow__controls-fitview').click({ timeout: 5_000 });
await node.click({ timeout: 5_000 });
}
await this.waitForInspectorLoaded();
}

/**
* The inspector shell renders immediately and fills in after an async fetch,
* so callers must not read it until the payload is in. `data-state` is set by
* the component; matching on it beats string-matching the loading copy.
*/
async waitForInspectorLoaded(timeoutMs = 20_000): Promise<void> {
await this.page.waitForSelector('[data-testid="lokee-object-inspector"][data-state="ready"]', {
timeout: timeoutMs,
});
}

async objectNamedVisible(name: string, timeoutMs = 5_000): Promise<boolean> {
return this.objectNode(name)
.waitFor({ state: 'visible', timeout: timeoutMs })
.then(() => true)
.catch(() => false);
}

async inspectorHasGrowth(): Promise<boolean> {
return this.page.locator('[data-testid="lokee-inspector-growth"]').isVisible();
}

async inspectorHasSource(): Promise<boolean> {
return this.page.locator('[data-testid="lokee-inspector-source"]').isVisible();
/** Sections render only once loaded, so callers must await the inspector first. */
async inspectorHasSection(section: 'growth' | 'source' | 'columns' | 'indexes'): Promise<boolean> {
return this.page.locator(`[data-testid="lokee-inspector-${section}"]`).isVisible();
}

async inspectorText(): Promise<string> {
Expand Down
16 changes: 15 additions & 1 deletion apps/e2e/src/tests/dialects/postgres.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ describe.skipIf(!hasConfig(DIALECT))(`Compare flow: ${DIALECT}`, () => {
runDialectFlow(
DIALECT,
() => getSourceConfig(DIALECT)!,
() => getTargetConfig(DIALECT)!
() => getTargetConfig(DIALECT)!,
{
// The demo_a→demo_b migrate adds fn_order_total and widens customers, so
// History has something to show. A routine must not report Table growth
// — that regression is the reason these assertions exist.
historyObjects: [
{
name: 'fn_order_total',
expectSource: true,
expectGrowth: false,
expectTimeline: /v\d+\s*·\s*ADD/i,
},
{ name: 'customers', expectGrowth: true },
],
}
);
});
42 changes: 31 additions & 11 deletions apps/e2e/src/tests/dialects/shared-flow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,31 @@ import { MigrationPage } from '../../pages/MigrationPage.js';
import { LokeeHistoryPage } from '../../pages/LokeeHistoryPage.js';
import type { DbConfig } from '../../helpers/db-config.js';

/** What the History inspector must show for one object after migrate. */
export interface HistoryObjectExpectation {
/** Node label as rendered in the graph, e.g. `fn_order_total`. */
name: string;
/** Routines have a Source section; tables do not. */
expectSource?: boolean;
/** Table growth is meaningless for a routine and must not be shown. */
expectGrowth: boolean;
/** Matched against the inspector's timeline text, e.g. /v\d+\s*·\s*ADD/i. */
expectTimeline?: RegExp;
}

export interface DialectFlowOptions {
/**
* Skip execute/history steps. Used for dialects whose adapter is SELECT-only
* in the SQL Editor / e2e path (e.g. SQLite) so compare still gets coverage.
*/
skipMigration?: boolean;
/**
* Objects to open in the History inspector after migrate, and what each must
* show. Expectations depend on the seed, so they live with the dialect that
* chooses it rather than as a dialect-name check inside this shared flow —
* another dialect running the same seed opts in with one line.
*/
historyObjects?: HistoryObjectExpectation[];
}

export function runDialectFlow(
Expand Down Expand Up @@ -225,18 +244,19 @@ export function runDialectFlow(
await driver.locator('[data-testid^="rf-version-"]').first().waitFor({ timeout: 10_000 });
expect(await driver.locator('[data-testid^="rf-version-"]').count()).toBeGreaterThan(0);

// Postgres demo_a→demo_b migrate adds fn_order_total and widens customers.
// Functions must not show Table growth; tables must.
// Per-object inspector expectations, supplied by the dialect that knows
// its seed. Empty for dialects that have not opted in.
const history = new LokeeHistoryPage(driver);
if (await history.objectNamedVisible('fn_order_total')) {
await history.clickObjectNamed('fn_order_total');
expect(await history.inspectorHasSource()).toBe(true);
expect(await history.inspectorHasGrowth()).toBe(false);
expect(await history.inspectorText()).toMatch(/v\d+\s*·\s*ADD/i);
}
if (await history.objectNamedVisible('customers')) {
await history.clickObjectNamed('customers');
expect(await history.inspectorHasGrowth()).toBe(true);
for (const expected of options.historyObjects ?? []) {
if (!(await history.objectNamedVisible(expected.name))) continue;
await history.clickObjectNamed(expected.name);
if (expected.expectSource !== undefined) {
expect(await history.inspectorHasSection('source')).toBe(expected.expectSource);
}
expect(await history.inspectorHasSection('growth')).toBe(expected.expectGrowth);
if (expected.expectTimeline) {
expect(await history.inspectorText()).toMatch(expected.expectTimeline);
}
}
}
await clickWhen(driver, '[data-testid="sync-pane-compare-btn"]');
Expand Down
4 changes: 2 additions & 2 deletions apps/e2e/src/tests/schema-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ describe.skipIf(!ready)('Schema Sync · History (SQLite)', () => {
});

it('opens a table blueprint with columns and a script diff', async () => {
await history.clickTableNode('customers');
await history.clickObjectNamed('customers');
const text = await history.inspectorText();
expect(text).toMatch(/email/i);
expect(await driver.locator('[data-testid="lokee-inspector-columns"]').isVisible()).toBe(true);
Expand Down Expand Up @@ -151,7 +151,7 @@ describe.skipIf(!ready)('Schema Sync · History (SQLite)', () => {
await expect
.poll(async () => history.versionCount(), { timeout: 20_000 })
.toBeGreaterThan(before);
await history.clickTableNode('customers');
await history.clickObjectNamed('customers');
const inspector = await history.inspectorText();
expect(inspector).toMatch(/phone/i);
expect(await driver.locator('[data-testid="lokee-inspector-growth"]').isVisible()).toBe(true);
Expand Down
Loading
Loading