mirror of
https://github.com/Canner/WrenAI.git
synced 2026-09-24 23:29:49 +08:00
feat(wren-ui/e2e): Add calculated fields CRUD test for E-commerce sample dataset (#427)
* chore(e2e): update .env.test * chore(e2e): update README.md for E2E * feat(wren-ui/e2e): add CF test for e-commerce
This commit is contained in:
+2
-1
@@ -1,2 +1,3 @@
|
||||
DB_TYPE=sqlite
|
||||
SQLITE_FILE=testdb.sqlite3
|
||||
SQLITE_FILE=testdb.sqlite3
|
||||
OTHER_SERVICE_USING_DOCKER=true
|
||||
+38
-36
@@ -1,49 +1,52 @@
|
||||
## How to run e2e test locally
|
||||
|
||||
1. Make sure you have start all WrenAI services. ([How to start](https://github.com/Canner/WrenAI/blob/3234dc218b105caba04e1cdab7b1cb7140fc9b90/docker/README.md#how-to-start))
|
||||
|
||||
|
||||
2. Create a `e2e.config.json` file under `wren-ui/e2e` folder and replace all data sources needed values in `./config.ts`.
|
||||
|
||||
```ts
|
||||
// Replace the default test config with your own e2e.config.json
|
||||
const defaultTestConfig = {
|
||||
bigQuery: {
|
||||
projectId: 'wrenai',
|
||||
datasetId: 'wrenai.tpch_sf1',
|
||||
// The credential file should be under "wren-ui" folder
|
||||
// For example: .tmp/credential.json
|
||||
credentialPath: 'bigquery-credential-path',
|
||||
},
|
||||
duckDb: {
|
||||
sqlCsvPath: 'https://duckdb.org/data/flights.csv',
|
||||
},
|
||||
postgreSql: {
|
||||
host: 'postgresql-host',
|
||||
port: '5432',
|
||||
username: 'postgresql-username',
|
||||
password: 'postgresql-password',
|
||||
database: 'postgresql-database',
|
||||
ssl: false,
|
||||
},
|
||||
};
|
||||
```
|
||||
```ts
|
||||
// Replace the default test config with your own e2e.config.json
|
||||
const defaultTestConfig = {
|
||||
bigQuery: {
|
||||
projectId: 'wrenai',
|
||||
datasetId: 'wrenai.tpch_sf1',
|
||||
// The credential file should be under "wren-ui" folder
|
||||
// For example: .tmp/credential.json
|
||||
credentialPath: 'bigquery-credential-path',
|
||||
},
|
||||
duckDb: {
|
||||
sqlCsvPath: 'https://duckdb.org/data/flights.csv',
|
||||
},
|
||||
postgreSql: {
|
||||
host: 'postgresql-host',
|
||||
port: '5432',
|
||||
username: 'postgresql-username',
|
||||
password: 'postgresql-password',
|
||||
database: 'postgresql-database',
|
||||
ssl: false,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
3. Build UI before starting e2e server
|
||||
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
```bash
|
||||
yarn build
|
||||
```
|
||||
|
||||
> Ensure port 3000 is available for E2E testing. The AI service needs WREN_UI_ENDPOINT to connect to this port for accurate and reliable test results.
|
||||
|
||||
4. Run test
|
||||
|
||||
```bash
|
||||
yarn test:e2e
|
||||
```
|
||||
```bash
|
||||
yarn test:e2e
|
||||
```
|
||||
|
||||
Run test with browser open
|
||||
|
||||
```bash
|
||||
yarn test:e2e --headed
|
||||
```
|
||||
Run test with browser open
|
||||
|
||||
```bash
|
||||
yarn test:e2e --headed
|
||||
```
|
||||
|
||||
## How to develop
|
||||
|
||||
@@ -64,4 +67,3 @@
|
||||
```
|
||||
npx playwright codegen http://localhost:3000
|
||||
```
|
||||
|
||||
|
||||
@@ -89,12 +89,7 @@ export const executeModelCRUD = async (
|
||||
).toBeHidden();
|
||||
|
||||
// add the model back
|
||||
await page
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Models\(\d\)$/ })
|
||||
.locator('path')
|
||||
.first()
|
||||
.click();
|
||||
await page.getByTestId('add-model').click();
|
||||
|
||||
// chkeck Model drawer open
|
||||
await expect(page.locator('.ant-drawer-mask')).toBeVisible();
|
||||
@@ -546,3 +541,120 @@ export const updateViewMetadata = async (
|
||||
|
||||
await checkDeployUndeployedChanges({ page, baseURL });
|
||||
};
|
||||
|
||||
export const addCalculatedField = async (
|
||||
page: Page,
|
||||
{
|
||||
calculatedFieldName,
|
||||
expression,
|
||||
modelDisplayName,
|
||||
toFieldModelDisplayName,
|
||||
toFieldColumnDisplayName,
|
||||
}: {
|
||||
calculatedFieldName: string;
|
||||
expression: string;
|
||||
modelDisplayName: string;
|
||||
toFieldModelDisplayName: string;
|
||||
toFieldColumnDisplayName: string;
|
||||
},
|
||||
) => {
|
||||
// click the model of sidebar to zoom in
|
||||
await page
|
||||
.getByRole('complementary')
|
||||
.getByText(modelDisplayName, { exact: true })
|
||||
.click();
|
||||
|
||||
// add calculated field
|
||||
await page
|
||||
.getByTestId(`diagram__model-node__${modelDisplayName}`)
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Calculated Fields$/ })
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(page.locator('.ant-modal-mask')).toBeVisible();
|
||||
await expect(page.locator('div.ant-modal')).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.locator('div.ant-modal-title')
|
||||
.filter({ hasText: 'Add calculated field' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('Add calculated field')
|
||||
.getByLabel('Close', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill(calculatedFieldName);
|
||||
|
||||
await page.getByTestId('common__descriptive-select').click();
|
||||
await page.getByTitle(expression).locator('div').click();
|
||||
|
||||
await expect(page.getByTestId('common__lineage')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('common__lineage-field-block')
|
||||
.getByText(modelDisplayName, { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId('common__lineage-fields-select').click();
|
||||
|
||||
// for skip disabled item
|
||||
await page.getByTestId('common__lineage-fields-select').press('ArrowDown');
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldModelDisplayName })
|
||||
.scrollIntoViewIfNeeded();
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldModelDisplayName })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('common__lineage-field-block')
|
||||
.getByText(toFieldModelDisplayName, { exact: true }),
|
||||
).toHaveCount(2);
|
||||
|
||||
await expect(page.getByText('Please select a field.')).toBeVisible();
|
||||
await page.getByTestId('common__lineage-fields-select').last().click();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldColumnDisplayName })
|
||||
.scrollIntoViewIfNeeded();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldColumnDisplayName })
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(
|
||||
page.getByText('Successfully created calculated field.'),
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
export const deleteCalculatedField = async (
|
||||
page: Page,
|
||||
modelDisplayName: string,
|
||||
) => {
|
||||
// delete calculated field
|
||||
await page
|
||||
.getByRole('complementary')
|
||||
.getByText(modelDisplayName, { exact: true })
|
||||
.click();
|
||||
await page
|
||||
.getByTestId(`diagram__model-node__${modelDisplayName}`)
|
||||
.getByRole('button', { name: 'more' })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByText('Delete', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await expect(
|
||||
page.getByText('Successfully deleted calculated field.'),
|
||||
).toBeVisible();
|
||||
};
|
||||
|
||||
@@ -125,4 +125,85 @@ test.describe('Test E-commerce sample dataset', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('Calculated Fields CRUD successfully', async ({ page }) => {
|
||||
await page.goto('/modeling');
|
||||
await expect(page).toHaveURL('/modeling', { timeout: 60000 });
|
||||
|
||||
const modelDisplayName = 'Orders';
|
||||
const calculatedFieldName = 'Sum of review scores';
|
||||
const expression = 'Sum';
|
||||
const toFieldModelDisplayName = 'reviews';
|
||||
const toFieldColumnDisplayName = 'Score';
|
||||
|
||||
const newCfName = 'total product items';
|
||||
const newExpression = 'COUNT';
|
||||
const newToFieldModelDisplayName = 'order_items';
|
||||
const newToFieldColumnDisplayName = 'OrderId';
|
||||
|
||||
await modelingHelper.addCalculatedField(page, {
|
||||
calculatedFieldName,
|
||||
expression,
|
||||
modelDisplayName,
|
||||
toFieldModelDisplayName,
|
||||
toFieldColumnDisplayName,
|
||||
});
|
||||
|
||||
// update calculated field
|
||||
await page
|
||||
.getByTestId(`diagram__model-node__${modelDisplayName}`)
|
||||
.getByRole('button', { name: 'more' })
|
||||
.nth(1)
|
||||
.click();
|
||||
|
||||
await page.getByText('Edit').click();
|
||||
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill(newCfName);
|
||||
|
||||
await page.getByTestId('common__descriptive-select').click();
|
||||
await page.getByTitle(newExpression).locator('div').click();
|
||||
|
||||
await page
|
||||
.getByTestId('common__lineage-field-block')
|
||||
.filter({ hasText: modelDisplayName })
|
||||
.getByText(toFieldModelDisplayName, { exact: true })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: newToFieldModelDisplayName })
|
||||
.scrollIntoViewIfNeeded();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: newToFieldModelDisplayName })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('common__lineage-field-block')
|
||||
.getByText(newToFieldModelDisplayName, { exact: true }),
|
||||
).toHaveCount(2);
|
||||
await expect(page.getByText('Please select a field.')).toBeVisible();
|
||||
await page.getByTestId('common__lineage-fields-select').last().click();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: newToFieldColumnDisplayName })
|
||||
.scrollIntoViewIfNeeded();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: newToFieldColumnDisplayName })
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(
|
||||
page.getByText('Successfully updated calculated field.'),
|
||||
).toBeVisible();
|
||||
|
||||
// delete calculated field
|
||||
await modelingHelper.deleteCalculatedField(page, modelDisplayName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -132,90 +132,20 @@ test.describe('Test NBA sample dataset', () => {
|
||||
await expect(page).toHaveURL('/modeling', { timeout: 60000 });
|
||||
|
||||
const modelDisplayName = 'game';
|
||||
const cfName = 'count of games';
|
||||
const calculatedFieldName = 'count of games';
|
||||
const expression = 'Sum';
|
||||
const toFieldModelDisplayName = 'line_score';
|
||||
const toFieldColumnDisplayName = 'GameId';
|
||||
const newToFieldModelDisplayName = 'player_games';
|
||||
const newToFieldColumnDisplayName = 'GameID';
|
||||
|
||||
await page
|
||||
.getByRole('complementary')
|
||||
.getByText(modelDisplayName, { exact: true })
|
||||
.click();
|
||||
|
||||
// add calculated field
|
||||
await page
|
||||
.getByTestId(`diagram__model-node__${modelDisplayName}`)
|
||||
.locator('div')
|
||||
.filter({ hasText: /^Calculated Fields$/ })
|
||||
.getByRole('button')
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await expect(page.locator('.ant-modal-mask')).toBeVisible();
|
||||
await expect(page.locator('div.ant-modal')).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.locator('div.ant-modal-title')
|
||||
.filter({ hasText: 'Add calculated field' }),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page
|
||||
.getByLabel('Add calculated field')
|
||||
.getByLabel('Close', { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByLabel('Name').click();
|
||||
await page.getByLabel('Name').fill(cfName);
|
||||
|
||||
await page.getByTestId('common__descriptive-select').click();
|
||||
await page.getByTitle(expression).locator('div').click();
|
||||
|
||||
await expect(page.getByTestId('common__lineage')).toBeVisible();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('common__lineage-field-block')
|
||||
.getByText(modelDisplayName, { exact: true }),
|
||||
).toBeVisible();
|
||||
|
||||
await page.getByTestId('common__lineage-fields-select').click();
|
||||
|
||||
// for skip disabled item
|
||||
await page.getByTestId('common__lineage-fields-select').press('ArrowDown');
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldModelDisplayName })
|
||||
.scrollIntoViewIfNeeded();
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldModelDisplayName })
|
||||
.click();
|
||||
|
||||
await expect(
|
||||
page
|
||||
.getByTestId('common__lineage-field-block')
|
||||
.getByText(toFieldModelDisplayName, { exact: true }),
|
||||
).toHaveCount(2);
|
||||
|
||||
await expect(page.getByText('Please select a field.')).toBeVisible();
|
||||
await page.getByTestId('common__lineage-fields-select').last().click();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldColumnDisplayName })
|
||||
.scrollIntoViewIfNeeded();
|
||||
|
||||
await page
|
||||
.getByTestId('common__fields__select-option')
|
||||
.filter({ hasText: toFieldColumnDisplayName })
|
||||
.click();
|
||||
|
||||
await page.getByRole('button', { name: 'Save' }).click();
|
||||
await expect(
|
||||
page.getByText('Successfully created calculated field.'),
|
||||
).toBeVisible();
|
||||
await modelingHelper.addCalculatedField(page, {
|
||||
calculatedFieldName,
|
||||
expression,
|
||||
modelDisplayName,
|
||||
toFieldModelDisplayName,
|
||||
toFieldColumnDisplayName,
|
||||
});
|
||||
|
||||
// update calculated field
|
||||
await page
|
||||
@@ -279,19 +209,6 @@ test.describe('Test NBA sample dataset', () => {
|
||||
).toBeVisible();
|
||||
|
||||
// delete calculated field
|
||||
await page
|
||||
.getByRole('complementary')
|
||||
.getByText(modelDisplayName, { exact: true })
|
||||
.click();
|
||||
await page
|
||||
.getByTestId(`diagram__model-node__${modelDisplayName}`)
|
||||
.getByRole('button', { name: 'more' })
|
||||
.nth(1)
|
||||
.click();
|
||||
await page.getByText('Delete', { exact: true }).click();
|
||||
await page.getByRole('button', { name: 'Delete' }).click();
|
||||
await expect(
|
||||
page.getByText('Successfully deleted calculated field.'),
|
||||
).toBeVisible();
|
||||
await modelingHelper.deleteCalculatedField(page, modelDisplayName);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export default defineConfig({
|
||||
|
||||
use: {
|
||||
// Base URL to use in actions like `await page.goto('/')`.
|
||||
baseURL: 'http://127.0.0.1:3333',
|
||||
baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
// Collect trace when retrying the failed test.
|
||||
trace: 'on-first-retry',
|
||||
@@ -45,8 +45,8 @@ export default defineConfig({
|
||||
],
|
||||
// Run your local dev server before starting the tests.
|
||||
webServer: {
|
||||
command: 'NODE_ENV=test yarn start -p 3333',
|
||||
url: 'http://127.0.0.1:3333',
|
||||
command: 'NODE_ENV=test yarn start -p 3000',
|
||||
url: 'http://127.0.0.1:3000',
|
||||
reuseExistingServer: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -113,7 +113,12 @@ export default function ModelTree(props: Props) {
|
||||
},
|
||||
{
|
||||
key: 'add-model',
|
||||
icon: () => <PlusSquareOutlined onClick={() => onOpenModelDrawer()} />,
|
||||
icon: () => (
|
||||
<PlusSquareOutlined
|
||||
data-testid="add-model"
|
||||
onClick={() => onOpenModelDrawer()}
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user