/* eslint-disable @typescript-eslint/unbound-method */
import { testData } from '@neo4j-cypher/language-support';
import { expect, test } from '@playwright/experimental-ct-react';
import type { Page } from '@playwright/test';
import { CypherEditor } from '../CypherEditor';
test('hello world end 2 end test', async ({ mount }) => {
const component = await mount();
await expect(component).toContainText('hello world');
await component.update();
await expect(component).toContainText('RETURN 123');
});
test('can complete in the middle of statement', async ({ mount, page }) => {
const component = await mount(
,
);
// Move into the statement and trigger autocompletion
const textField = page.getByRole('textbox');
await textField.focus();
await textField.press('ArrowDown');
await textField.press('ArrowRight');
await textField.press('ArrowRight');
await textField.press('ArrowRight');
await textField.press('ArrowRight');
await textField.press('Control+ ');
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
await page.locator('.cm-tooltip-autocomplete').getByText('WHERE', {exact: true}).click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('WHERE true');
});
test('get completions when typing and can accept completions with tab', async ({
mount,
page,
}) => {
const component = await mount();
const textField = page.getByRole('textbox');
await textField.fill('RETU');
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('RETURN'),
).toBeVisible();
// We need to wait for the editor to realise there is a completion open
// so that it does not just indent with tab key
await page.waitForTimeout(500);
await textField.press('Tab');
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('RETURN');
});
test('get completions when typing in controlled component', async ({
mount,
page,
}) => {
let value = '';
const onChange = (val: string) => {
value = val;
void component.update();
};
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('RETU');
await page.waitForTimeout(500); // wait for debounce
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('RETURN'),
).toBeVisible();
// We need to wait for the editor to realise there is a completion open
// so that it does not just indent with tab key
await page.waitForTimeout(500);
await textField.press('Tab');
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('RETURN');
});
test('can complete labels', async ({ mount, page }) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('MATCH (n :P');
await page.locator('.cm-tooltip-autocomplete').getByText('Pokemon').click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('MATCH (n :Pokemon');
});
test('can complete properties with backticks', async ({ mount, page }) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('MATCH (n) RETURN n.foo');
await textField.press('Escape');
await textField.press('Control+ ');
await page.locator('.cm-tooltip-autocomplete').getByText('foo bar').click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('MATCH (n) RETURN n.`foo bar`');
});
test('can update dbschema', async ({ mount, page }) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('MATCH (n :');
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('Pokemon'),
).toBeVisible();
await textField.press('Escape');
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await component.update(
,
);
await textField.press('Control+ ');
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('Pokemon'),
).toBeVisible();
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('Digimon'),
).toBeVisible();
});
test('can complete rel types', async ({ page, mount }) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('MATCH (n)-[:');
await page.locator('.cm-tooltip-autocomplete').getByText('KNOWS').click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('MATCH (n)-[:KNOWS');
});
test('can complete YIELD clauses without manual trigger', async ({
page,
mount,
}) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('CALL dbms.components() YIELD ');
await page.locator('.cm-tooltip-autocomplete').getByText('edition').click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('CALL dbms.components() YIELD edition');
});
test('automatic yield trigger is not case sensitive', async ({
page,
mount,
}) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('CALL dbms.components() yIeLd ');
await page.locator('.cm-tooltip-autocomplete').getByText('edition').click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('CALL dbms.components() yIeLd edition');
});
test('can complete functions', async ({ page, mount }) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('RETURN func');
await page
.locator('.cm-tooltip-autocomplete')
.getByText('function123')
.click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('RETURN function123');
});
test('can complete procedures', async ({ page, mount }) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('CALL d');
await page.locator('.cm-tooltip-autocomplete').getByText('db.ping').click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('CALL db.ping');
});
test('can complete parameters', async ({ page, mount }) => {
const component = await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('RETURN $p');
await page.locator('.cm-tooltip-autocomplete').getByText('parameter').click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
await expect(component).toContainText('RETURN $parameter');
});
test('completes allShortestPaths correctly', async ({ page, mount }) => {
await mount(
,
);
const textField = page.getByRole('textbox');
// The first query contains errors on purpose so the
// syntax errors get triggered before the auto-completion
await textField.fill('MATCH (n) REURN n; MATCH a');
await page
.locator('.cm-tooltip-autocomplete')
.getByText('allShortestPaths')
.click();
await expect(page.locator('.cm-tooltip-autocomplete')).not.toBeVisible();
expect(await textField.textContent()).toEqual(
'MATCH (n) REURN n; MATCH allShortestPaths',
);
});
async function getInfoTooltip(page: Page, methodName: string) {
const infoTooltip = page.locator('.cm-completionInfo');
const firstOption = page.locator('li[aria-selected="true"]');
let selectedOption = firstOption;
while (!(await infoTooltip.textContent()).includes(methodName)) {
await page.keyboard.press('ArrowDown');
const currentSelected = page.locator('li[aria-selected="true"]');
expect(currentSelected).not.toBe(selectedOption);
expect(currentSelected).not.toBe(firstOption);
selectedOption = currentSelected;
}
return infoTooltip;
}
test('shows signature help information on auto-completion for procedures', async ({
page,
mount,
}) => {
await mount();
const procName = 'apoc.periodic.iterate';
const procedure = testData.mockSchema.procedures['CYPHER 5'][procName];
const textField = page.getByRole('textbox');
await textField.fill('CALL apoc.periodic.');
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
const infoTooltip = await getInfoTooltip(page, procName);
await expect(infoTooltip).toContainText(procedure.signature);
await expect(infoTooltip).toContainText(procedure.description);
});
test('shows signature help information on auto-completion for functions', async ({
page,
mount,
}) => {
await mount();
const fnName = 'apoc.coll.combinations';
const fn = testData.mockSchema.functions['CYPHER 5'][fnName];
const textField = page.getByRole('textbox');
await textField.fill('RETURN apoc.coll.');
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
const infoTooltip = await getInfoTooltip(page, fnName);
await expect(infoTooltip).toContainText(fn.signature);
await expect(infoTooltip).toContainText(fn.description);
});
test('shows deprecated procedures as strikethrough on auto-completion', async ({
page,
mount,
}) => {
const procName = 'apoc.trigger.resume';
await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('CALL apoc.trigger.');
// We need to assert on the element having the right class
// and trusting the CSS is making this truly strikethrough
await expect(page.locator('.cm-deprecated-element')).toBeVisible();
});
test('shows deprecated function as strikethrough on auto-completion', async ({
page,
mount,
}) => {
const fnName = 'apoc.create.uuid';
await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('RETURN apoc.create.');
// We need to assert on the element having the right class
// and trusting the CSS is making this truly strikethrough
await expect(page.locator('.cm-deprecated-element')).toBeVisible();
});
test('does not signature help information on auto-completion if docs and signature are empty', async ({
page,
mount,
}) => {
await mount();
const textField = page.getByRole('textbox');
await textField.fill('C');
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
await expect(page.locator('.cm-completionInfo')).not.toBeVisible();
});
test('shows signature help information on auto-completion if description is not empty, signature is', async ({
page,
mount,
}) => {
await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('CALL db.');
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
await expect(page.locator('.cm-completionInfo')).toBeVisible();
});
test('shows signature help information on auto-completion if signature is not empty, description is', async ({
page,
mount,
}) => {
await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('CALL db.');
await expect(page.locator('.cm-tooltip-autocomplete')).toBeVisible();
await expect(page.locator('.cm-completionInfo')).toBeVisible();
});
test('completions depend on the Cypher version', async ({ page, mount }) => {
await mount(
,
);
const textField = page.getByRole('textbox');
await textField.fill('CYPHER 5 RETURN cypher');
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('cypher5Function'),
).toBeVisible();
await textField.fill('CYPHER 25 RETURN cypher');
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('cypher25Function'),
).toBeVisible();
});
test('does not complete properties for non node / relationship variables', async ({ page, mount }) => {
await mount(
);
const textField = page.getByRole('textbox');
await textField.fill('MATCH (n) RETURN n.');
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('nodeProperty'),
).toBeVisible();
await textField.fill('WITH 1 AS x RETURN x.');
// This could be flaky if the semantic analysis takes too long
await page.waitForTimeout(500)
await textField.press('Escape');
await textField.press('Control+ ');
await expect(
page.locator('.cm-tooltip-autocomplete').getByText('nodeProperty')
).not.toBeVisible();
});