From 8faebc76bb4b2f3124ab23e4f0ef385fbd01f4a1 Mon Sep 17 00:00:00 2001 From: Freda Lai <42527625+fredalai@users.noreply.github.com> Date: Wed, 19 Jun 2024 17:02:02 +0800 Subject: [PATCH] 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 --- wren-ui/.env.test | 3 +- wren-ui/e2e/README.md | 74 ++++++----- wren-ui/e2e/commonTests/modeling.ts | 124 +++++++++++++++++- .../e2e/specs/connectSampleECommerce.spec.ts | 81 ++++++++++++ wren-ui/e2e/specs/connectSampleNBA.spec.ts | 101 ++------------ wren-ui/playwright.config.ts | 6 +- .../components/sidebar/modeling/ModelTree.tsx | 7 +- 7 files changed, 257 insertions(+), 139 deletions(-) diff --git a/wren-ui/.env.test b/wren-ui/.env.test index f43146af5..caf624b9e 100644 --- a/wren-ui/.env.test +++ b/wren-ui/.env.test @@ -1,2 +1,3 @@ DB_TYPE=sqlite -SQLITE_FILE=testdb.sqlite3 \ No newline at end of file +SQLITE_FILE=testdb.sqlite3 +OTHER_SERVICE_USING_DOCKER=true \ No newline at end of file diff --git a/wren-ui/e2e/README.md b/wren-ui/e2e/README.md index 071c0bc80..4ea81ef87 100644 --- a/wren-ui/e2e/README.md +++ b/wren-ui/e2e/README.md @@ -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 ``` - diff --git a/wren-ui/e2e/commonTests/modeling.ts b/wren-ui/e2e/commonTests/modeling.ts index fd1d4e4c7..5e7b0a518 100644 --- a/wren-ui/e2e/commonTests/modeling.ts +++ b/wren-ui/e2e/commonTests/modeling.ts @@ -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(); +}; diff --git a/wren-ui/e2e/specs/connectSampleECommerce.spec.ts b/wren-ui/e2e/specs/connectSampleECommerce.spec.ts index f2a325214..a5dae1c86 100644 --- a/wren-ui/e2e/specs/connectSampleECommerce.spec.ts +++ b/wren-ui/e2e/specs/connectSampleECommerce.spec.ts @@ -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); + }); }); diff --git a/wren-ui/e2e/specs/connectSampleNBA.spec.ts b/wren-ui/e2e/specs/connectSampleNBA.spec.ts index b8ccc261b..c0bd145fa 100644 --- a/wren-ui/e2e/specs/connectSampleNBA.spec.ts +++ b/wren-ui/e2e/specs/connectSampleNBA.spec.ts @@ -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); }); }); diff --git a/wren-ui/playwright.config.ts b/wren-ui/playwright.config.ts index 20a373e7f..9714b1fb9 100644 --- a/wren-ui/playwright.config.ts +++ b/wren-ui/playwright.config.ts @@ -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, }, }); diff --git a/wren-ui/src/components/sidebar/modeling/ModelTree.tsx b/wren-ui/src/components/sidebar/modeling/ModelTree.tsx index 74fec04dd..4a2a72a87 100644 --- a/wren-ui/src/components/sidebar/modeling/ModelTree.tsx +++ b/wren-ui/src/components/sidebar/modeling/ModelTree.tsx @@ -113,7 +113,12 @@ export default function ModelTree(props: Props) { }, { key: 'add-model', - icon: () => onOpenModelDrawer()} />, + icon: () => ( + onOpenModelDrawer()} + /> + ), }, ], });