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 @@ -126,6 +126,7 @@ const ALWAYS = [
'src/tests/sql-editor-peek-row-form.test.ts',
'src/tests/sql-editor-peek-stacking.test.ts',
'src/tests/sql-editor-compare-multi-target.test.ts',
'src/tests/sql-editor-join-typing.test.ts',
].join(' '),
label: 'SQL Editor',
},
Expand Down
55 changes: 50 additions & 5 deletions apps/e2e/src/generated/test-ids.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ export const TestIds = {
diffTablesList: (index: string | number) => `diff-tables-${index}-list`,
/** div */
diffUnsupported: 'diff-unsupported',
/** div · Permission Diff */
/** div · Insight */
permissionDiff: 'permission-diff',
},
PermissionInspector: {
Expand Down Expand Up @@ -2007,8 +2007,20 @@ export const TestIds = {
dataPeekGrid: (id: string | number) => `data-peek-grid-${id}`,
/** input */
dataPeekLimit: (id: string | number) => `data-peek-limit-${id}`,
/** input · ID DESC */
/** input · created_at */
dataPeekOrder: (id: string | number) => `data-peek-order-${id}`,
/** input · created_at */
dataPeekOrderForIdAndIndex: (id: string | number, index: string | number) => `data-peek-order-${id}-${index}`,
/** button · Add sort */
dataPeekOrderAdd: (id: string | number) => `data-peek-order-add-${id}`,
/** button · Remove this sort */
dataPeekOrderClear: (id: string | number) => `data-peek-order-clear-${id}`,
/** button · Remove this sort */
dataPeekOrderClearForIdAndIndex: (id: string | number, index: string | number) => `data-peek-order-clear-${id}-${index}`,
/** select · — ASC DESC */
dataPeekOrderDir: (id: string | number) => `data-peek-order-dir-${id}`,
/** select · — ASC DESC */
dataPeekOrderDirForIdAndIndex: (id: string | number, index: string | number) => `data-peek-order-dir-${id}-${index}`,
/** button */
dataPeekRefby: (table: string | number, fkKey: string | number) => `data-peek-refby-${table}-${fkKey}`,
/** div */
Expand All @@ -2023,10 +2035,16 @@ export const TestIds = {
dataPeekTabRows: (id: string | number) => `data-peek-tab-rows-${id}`,
/** div */
dataPeekTabs: (id: string | number) => `data-peek-tabs-${id}`,
/** input · e.g. ARCHIVED = false — or clear to fetch all */
/** input */
dataPeekWhere: (id: string | number) => `data-peek-where-${id}`,
/** button · Clear WHERE and fetch all rows */
/** input */
dataPeekWhereForIdAndIndex: (id: string | number, index: string | number) => `data-peek-where-${id}-${index}`,
/** button · Add condition */
dataPeekWhereAdd: (id: string | number) => `data-peek-where-add-${id}`,
/** button */
dataPeekWhereClear: (id: string | number) => `data-peek-where-clear-${id}`,
/** button */
dataPeekWhereClearForIdAndIndex: (id: string | number, index: string | number) => `data-peek-where-clear-${id}-${index}`,
},
EditorTabBar: {
/** div */
Expand Down Expand Up @@ -2291,6 +2309,16 @@ export const TestIds = {
sqlRecentOpen: (id: string | number) => `sql-recent-open-${id}`,
/** button · Recent */
sqlRecentTab: 'sql-recent-tab',
/** button · Delete workspace script */
sqlWorkspaceScriptDelete: (id: string | number) => `sql-workspace-script-delete-${id}`,
/** p */
sqlWorkspaceScriptError: 'sql-workspace-script-error',
/** button */
sqlWorkspaceScriptOpen: (id: string | number) => `sql-workspace-script-open-${id}`,
/** button · Save the current query for everyone in this workspace */
sqlWorkspaceScriptSave: 'sql-workspace-script-save',
/** button · Workspace */
sqlWorkspaceScriptsTab: 'sql-workspace-scripts-tab',
},
SqlEditorView: {
/** button · Save current query as a bookmark (uses the tab title) */
Expand Down Expand Up @@ -3479,7 +3507,7 @@ export const TestIds = {
workspaceMenuInviteDecline: (id: string | number) => `workspace-menu-invite-decline-${id}`,
/** div */
workspaceMenuInvites: 'workspace-menu-invites',
/** button */
/** option */
workspaceMenuItem: (id: string | number) => `workspace-menu-item-${id}`,
/** div */
workspaceMenuPublic: 'workspace-menu-public',
Expand All @@ -3489,6 +3517,8 @@ export const TestIds = {
workspaceMenuPublicEmpty: 'workspace-menu-public-empty',
/** button · Join */
workspaceMenuPublicJoin: (id: string | number) => `workspace-menu-public-join-${id}`,
/** select · Workspace */
workspaceMenuSelect: 'workspace-menu-select',
/** button · Workspace settings */
workspaceMenuSettings: 'workspace-menu-settings',
},
Expand Down Expand Up @@ -4015,6 +4045,12 @@ export type TestId =
| 'data-peek-insight-rows'
| `data-peek-limit-${string}`
| `data-peek-order-${string}`
| `data-peek-order-${string}-${string}`
| `data-peek-order-add-${string}`
| `data-peek-order-clear-${string}`
| `data-peek-order-clear-${string}-${string}`
| `data-peek-order-dir-${string}`
| `data-peek-order-dir-${string}-${string}`
| `data-peek-refby-${string}-${string}`
| `data-peek-referenced-by-${string}`
| `data-peek-resize-${string}`
Expand All @@ -4023,7 +4059,10 @@ export type TestId =
| `data-peek-tab-rows-${string}`
| `data-peek-tabs-${string}`
| `data-peek-where-${string}`
| `data-peek-where-${string}-${string}`
| `data-peek-where-add-${string}`
| `data-peek-where-clear-${string}`
| `data-peek-where-clear-${string}-${string}`
| 'db-access-all-form'
| 'db-access-all-note'
| 'db-access-all-target'
Expand Down Expand Up @@ -4872,6 +4911,11 @@ export type TestId =
| `sql-variable-value-edit-${string}`
| 'sql-variable-value-input'
| 'sql-variables'
| `sql-workspace-script-delete-${string}`
| 'sql-workspace-script-error'
| `sql-workspace-script-open-${string}`
| 'sql-workspace-script-save'
| 'sql-workspace-scripts-tab'
| 'sql-write-confirm'
| 'sql-write-confirm-btn'
| 'sql-write-confirm-cancel'
Expand Down Expand Up @@ -5178,6 +5222,7 @@ export type TestId =
| `workspace-menu-public-${string}`
| 'workspace-menu-public-empty'
| `workspace-menu-public-join-${string}`
| 'workspace-menu-select'
| 'workspace-menu-settings'
| 'workspace-rail-btn'
| 'workspace-rail-invites'
Expand Down
102 changes: 102 additions & 0 deletions apps/e2e/src/tests/sql-editor-join-typing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* Fox Schema (foxschema)
* Copyright 2024-2026 Huy Phan <huyplb@gmail.com>
* SPDX-License-Identifier: Apache-2.0
*
* Type a LEFT OUTER JOIN the way a person does, and check the popup at the
* spots that used to go quiet: after LEFT, after JOIN, and after `=`.
*/
import { describe, it, beforeAll, afterAll, expect } from 'vitest';
import type { Page } from 'playwright';
import { buildDriver, quitDriver } from '../helpers/driver.js';
import { AppPage } from '../pages/AppPage.js';
import { SqlEditorPage } from '../pages/SqlEditorPage.js';

let driver: Page;
let app: AppPage;
let sql: SqlEditorPage;
const pageErrors: string[] = [];

async function suggestText(): Promise<string> {
const widget = driver.locator('.suggest-widget').last();
const visible = await widget.isVisible().catch(() => false);
if (!visible) return '';
return (await widget.innerText()).replace(/\s+/g, ' ');
}

/** Type one character at a time. A space or dot can accept a suggestion, so dismiss first. */
async function typeLiteral(text: string): Promise<void> {
for (const ch of text) {
if (ch === ' ' || ch === '.') await driver.keyboard.press('Escape');
await driver.keyboard.type(ch, { delay: 12 });
}
}

beforeAll(async () => {
driver = await buildDriver();
driver.on('console', (msg) => {
if (msg.type() === 'error') pageErrors.push(msg.text());
});
driver.on('pageerror', (err) => pageErrors.push(String(err)));
app = new AppPage(driver);
sql = new SqlEditorPage(driver);
await app.open();
await sql.resetPersistedEditorState();
await driver.reload();
await driver.waitForSelector('[data-testid="toolbar"]');
await sql.openView();
}, 120_000);

afterAll(async () => {
await quitDriver(driver);
});

describe('typing a LEFT OUTER JOIN', () => {
it('keeps JOIN, tables, and alias columns available, and does not ask for a password', async () => {
await sql.dismissOverlays();
const editor = driver.locator('.monaco-editor').first();
await editor.click();
const mod = process.platform === 'darwin' ? 'Meta' : 'Control';
await driver.keyboard.press(`${mod}+KeyA`);
await driver.keyboard.press('Backspace');

const autocomplete = await driver
.locator('.monaco-editor textarea')
.first()
.getAttribute('autocomplete');
expect(autocomplete).toBe('one-time-code');

await typeLiteral('SELECT c.id, oc.coupon_id FROM demo_a.order_coupons oc left ');
await driver.waitForTimeout(500);
const afterLeft = await suggestText();
const soFar = (await sql.editorText()).replace(/\s+/g, ' ');
expect(
afterLeft.toUpperCase(),
`editor: ${soFar} errors: ${pageErrors.slice(0, 4).join(' | ')}`
).toMatch(/JOIN/);
expect(afterLeft.toUpperCase()).toMatch(/OUTER/);
await driver.keyboard.press('Escape');

await typeLiteral('outer join ');
await driver.waitForTimeout(500);
const afterJoin = await suggestText();
expect(afterJoin.length, 'no suggestions after JOIN').toBeGreaterThan(0);
await driver.keyboard.press('Escape');

await typeLiteral('demo_a.coupons c on c.id = ');
await driver.waitForTimeout(500);
const afterEq = (await suggestText()).toLowerCase();
expect(afterEq, 'no suggestions after =').toMatch(/c\.|oc\.|id|count/);
await driver.keyboard.press('Escape');

await typeLiteral('oc.coupon_id;');
await driver.keyboard.press('Escape');
await driver.waitForTimeout(500);

const text = (await sql.editorText()).replace(/\s+/g, ' ').toLowerCase();
expect(text).toContain('from demo_a.order_coupons oc');
expect(text).toContain('left outer join demo_a.coupons c');
expect(text).toContain('on c.id = oc.coupon_id');
expect(await driver.getByText('AutoFill for').count()).toBe(0);
});
});
4 changes: 2 additions & 2 deletions apps/e2e/src/tests/workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ afterAll(async () => {
describe('workspaces', () => {
it('the rail shows the account’s own workspace as current', async () => {
await openMenu();
expect(await driver.locator(byTestId(`workspace-menu-item-${personalId}`)).getAttribute('aria-current')).toBe('true');
expect(await driver.locator(byTestId('workspace-menu-select')).inputValue()).toBe(personalId);
});

it('an admin creates a workspace from the menu and lands in it', async () => {
Expand All @@ -81,7 +81,7 @@ describe('workspaces', () => {
created.push(ws.id);
expect(list.json.currentId).toBe(ws.id);
await openMenu();
expect(await driver.locator(byTestId(`workspace-menu-item-${ws.id}`)).getAttribute('aria-current')).toBe('true');
expect(await driver.locator(byTestId('workspace-menu-select')).inputValue()).toBe(ws.id);
await saveScreenshot(driver, 'workspaces-menu-new-current');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,7 @@ describe('AccessView — Permission Diff stale catalog', () => {
fetchDbAccess.mockResolvedValue(catalog('postgres', 'alice', 'only_on_postgres'));

render(<AccessView />);
expect(screen.getByTestId('access-tab-diff').textContent).toBe('Insight');
fireEvent.click(screen.getByTestId('access-tab-diff'));

fireEvent.change(screen.getByTestId('access-connection'), { target: { value: 'c1' } });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const SECTIONS: {
}[] = [
{ id: 'permission', label: 'Permissions', icon: ShieldCheck, permission: 'access.builder' },
{ id: 'users', label: 'Users', icon: UserCog, permission: 'access.users' },
{ id: 'diff', label: 'Diff', icon: GitCompare, permission: 'access.diff' },
{ id: 'diff', label: 'Insight', icon: GitCompare, permission: 'access.diff' },
];

function loadConnectionId(connections: { id: string }[]): string {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ export const PermissionDiff: React.FC<{
<div className="flex-1 flex min-h-0" data-testid="permission-diff">
<div className="w-[46%] min-w-[380px] overflow-y-auto border-r border-slate-800 p-5 flex flex-col gap-4">
<div>
<h2 className="text-sm font-bold text-slate-100">Permission Diff</h2>
<h2 className="text-sm font-bold text-slate-100">Insight</h2>
<ol className="text-[11px] text-slate-400 mt-1.5 list-decimal pl-4 flex flex-col gap-0.5">
<li>Choose the database, then the user or role.</li>
<li>Load catalog — that reads the privileges this database already granted.</li>
Expand Down Expand Up @@ -344,7 +344,7 @@ export const PermissionDiff: React.FC<{

<div className="flex-1 flex flex-col min-h-0 min-w-0">
<div className="flex items-center justify-between border-b border-slate-800 px-4 py-2 shrink-0">
<span className="text-xs font-bold text-slate-300">Diff &amp; SQL</span>
<span className="text-xs font-bold text-slate-300">Insight &amp; SQL</span>
<button
type="button"
data-testid="diff-copy-sql"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,24 @@ describe('DataPeekPanel Insight tab', () => {
await waitFor(() => expect(fetchTableInsight).toHaveBeenCalledTimes(1));
expect(screen.getByTestId('data-peek-insight')).toBeTruthy();
});

it('adds a second WHERE and a second ORDER BY', async () => {
render(<DataPeekPanel />);
fireEvent.click(screen.getByTestId('data-peek-where-add-e1'));
fireEvent.change(screen.getByTestId('data-peek-where-e1'), { target: { value: "status = 'gold'" } });
fireEvent.change(screen.getByTestId('data-peek-where-e1-1'), { target: { value: 'active = 1' } });
fireEvent.click(screen.getByTestId('data-peek-order-add-e1'));
fireEvent.change(screen.getByTestId('data-peek-order-e1'), { target: { value: 'created_at' } });
fireEvent.change(screen.getByTestId('data-peek-order-dir-e1'), { target: { value: 'DESC' } });
fireEvent.change(screen.getByTestId('data-peek-order-e1-1'), { target: { value: 'id' } });
fireEvent.change(screen.getByTestId('data-peek-order-dir-e1-1'), { target: { value: 'ASC' } });
fireEvent.click(screen.getByTestId('data-peek-apply-e1'));
await waitFor(() => {
const peek = useSqlEditorStore.getState().dataPeek;
expect(peek?.entries[0]?.whereClause).toBe("status = 'gold' AND active = 1");
expect(peek?.entries[0]?.orderByClause).toBe('created_at DESC, id ASC');
});
});
});

vi.mock('@/features/compare/state/useSyncStore', () => ({
Expand Down
Loading
Loading