diff --git a/web-local/tests/UI/check-inspector-source-model.spec.ts b/web-local/tests/UI/check-inspector-source-model.spec.ts new file mode 100644 index 00000000000..ef6142ddcdf --- /dev/null +++ b/web-local/tests/UI/check-inspector-source-model.spec.ts @@ -0,0 +1,132 @@ +import { test, expect } from '@playwright/test'; +import { test as RillTest } from '../utils/test'; +import { cloud, waitForTable } from '../utils/sourceHelpers'; +import { checkInspectorSource, checkInspectorModel } from '../utils/inspectorHelpers'; +import { createModel } from '../utils/modelHelpers'; + +// Testing the contents of the Inspector Panel +// Does the correct rows and columns appear, and does each column have a visible graph? +test.describe('Checking the Inspector Panel for Source and Model. Check if values are correct as well as if the UI populates graph.', () => { + RillTest('Reading Source into Rill from GCS', async ({ page }) => { + console.log('Testing cloud sales data ingestion...'); + await Promise.all([ + waitForTable(page, '/sources/sales.yaml', [ + 'sale_date', + 'sale_id', + 'duration_ms', + 'customer_id', + 'sales_amount_usd', + 'products', + 'discounts', + 'region', + 'is_online', + ]), + cloud(page, 'sales.csv', 'gcs'), + ]); + console.log('Sales table validated.'); + + await checkInspectorSource(page, '100,000', '9', + [ + 'sale_date', + 'sale_id', + 'duration_ms', + 'customer_id', + 'sales_amount_usd', + 'products', + 'discounts', + 'region', + 'is_online' + ] + ) + console.log('Testing cloud customer data ingestion...'); + await Promise.all([ + waitForTable(page, '/sources/customer_data.yaml', [ + 'customer_id', + 'name', + 'email', + 'signup_date', + 'preferences', + 'total_spent_usd', + 'loyalty_tier', + 'is_active', + ]), + cloud(page, 'customer_data.csv', 'gcs'), + ]); + console.log('Customer data table validated.'); + await checkInspectorSource(page, '10,000', '8', + [ + 'signup_date', + 'customer_id', + 'name', + 'email', + 'preferences', + 'total_spent_usd', + 'loyalty_tier', + 'is_active', + ] + ), + console.log("Creating model to join sources.") + await createModel(page, 'joined_model.sql'); + // wait for textbox to appear for model + await page.waitForSelector('div[role="textbox"]'); + + await page.evaluate(() => { + // Ensure the parent textbox is focused for typing + const parentTextbox = document.querySelector('div[role="textbox"]'); + if (parentTextbox) { + parentTextbox.focus(); + } else { + console.error("Parent textbox not found!"); + } + }); + + // Mimic typing in the child contenteditable div + const childTextbox = await page.locator('div[role="textbox"] div.cm-content'); + await childTextbox.click(); // Ensure it's focused for typing + + // Clear existing contents + await childTextbox.press('Meta+A'); // need to check this + await childTextbox.press('Backspace'); // Delete selected text + + const lines = [ + "-- Model SQL", + "-- Reference documentation: https://docs.rilldata.com/reference/project-files/models", + "SELECT a.*,", + " b.* exclude customer_id", + "FROM sales AS a", + "LEFT JOIN customer_data AS b", + "ON a.customer_id = b.customer_id", + "", "" + ]; + + // Type each line with a newline after + for (const line of lines) { + await childTextbox.type(line); // Type the line + await childTextbox.press('Enter'); // Press Enter for a new line + } + + console.log("Content typed successfully."); + await checkInspectorModel(page, '100,000', '16', + [ + 'sale_date', + 'sale_id', + 'duration_ms', + 'customer_id', + 'sales_amount_usd', + 'products', + 'discounts', + 'region', + 'is_online', + 'signup_date', + 'customer_id', + 'name', + 'email', + 'preferences', + 'total_spent_usd', + 'loyalty_tier', + 'is_active', + ] + ); + }); + +}); diff --git a/web-local/tests/UI/check-model-ui-buttons.spec.ts b/web-local/tests/UI/check-model-ui-buttons.spec.ts new file mode 100644 index 00000000000..1f9ee65f839 --- /dev/null +++ b/web-local/tests/UI/check-model-ui-buttons.spec.ts @@ -0,0 +1,155 @@ +import { test, expect } from '@playwright/test'; +import { test as RillTest } from '../utils/test'; +import { cloud, waitForTable } from '../utils/sourceHelpers'; +import { waitForFileNavEntry } from "../utils/waitHelpers"; +import { actionUsingMenu, checkExistInConnector, renameFileUsingMenu } from "../utils/commonHelpers"; + +// GCS source ingestion test +// based on public bucket gs://playwright-gcs-qa/* +// Can add more files as required, currently parquet.gz files are erroring so removed. + + +test.describe('Check Source UI buttons.', () => { + RillTest('Reading Source into Rill from GCS', async ({ page }) => { + console.log('Testing cloud sales data ingestion...'); + await Promise.all([ + waitForTable(page, '/sources/sales.yaml', [ + 'sale_date', + 'sale_id', + 'duration_ms', + 'customer_id', + 'sales_amount_usd', + 'products', + 'discounts', + 'region', + 'is_online', + ]), + cloud(page, 'sales.csv', 'gcs'), + ]); + console.log('Sales table validated...'); + + // Create Model! + console.log("Creating Create Model Button...") + await Promise.all([ + waitForFileNavEntry(page, "/models/sales_model.sql", false), //set true? + page.getByRole('button', { name: 'Create model' }).click() + ]); + + // CHECK CONNECTORS for MODEL (table name dynamic so wildcard) + await checkExistInConnector(page, 'duckdb', 'main_db', 'sales_model') + + + // CHECKING BUTTONS + //Close File Explore Sidebar + await page.locator('span[aria-label="Close sidebar"]').click(); + // Assert that the class changes + const sidebarClose = page.locator('.sidebar.svelte-5nrsv4'); + await expect(sidebarClose).toHaveClass('sidebar svelte-5nrsv4 hide'); + + + await page.locator('span[aria-label="Show sidebar"]').click(); + // Assert that the class changes + const sidebarOpen = page.locator('.sidebar.svelte-5nrsv4'); + await expect(sidebarOpen).toHaveClass('sidebar svelte-5nrsv4'); + + + // checking the refresh button + await page.locator('button[aria-label="Refresh Model"]').click(); //#6316, need to find where this gets added + await expect(page.getByText('Building model sales_model').first().isVisible()).toBeTruthy(); // Test will fail if the text is not visible + + // checking the panels , + await page.getByRole('button', { name: 'Toggle table visibility' }).click(); // #6308 + const resultsPreviewTable = await page.locator('[aria-label="Results Preview Table"]'); // #6316 + await expect(resultsPreviewTable).toBeHidden(); + await expect(resultsPreviewTable.locator(`text="sale_id"`)).toHaveCount(0); + + await page.getByRole('button', { name: 'Toggle inspector visibility' }).click(); // #6308 + const inspectorPanel = await page.locator('[aria-label="Inspector Panel"]'); // #6316 + await expect(inspectorPanel).toBeHidden(); + await expect(inspectorPanel.locator(`text="rows"`)).toHaveCount(0); + + + + // Wait for the download and confirm success (CSV, XLSX, Parquet) + const [downloadCSV] = await Promise.all([ + page.waitForEvent('download'), // Wait for the download event + page.getByLabel('Export Model Data').click(), // Dropdown + page.getByRole('menuitem', { name: 'Export as CSV' }).click()// Export + ]); + + const filePathCSV = await downloadCSV.path(); + if (filePathCSV) { + console.log(`File successfully downloaded to: ${filePathCSV}`); + } else { + console.error('Download failed.'); + } + + const [downloadParquet] = await Promise.all([ + page.waitForEvent('download'), // Wait for the download event + page.getByLabel('Export Model Data').click(), // Dropdown + page.locator('div[role="menuitem"]:has-text("Export as Parquet")').click()// Export + ]); + + const filePathParquet = await downloadParquet.path(); + if (filePathParquet) { + console.log(`File successfully downloaded to: ${filePathParquet}`); + } else { + console.error('Download failed.'); + } + + const [downloadXSLX] = await Promise.all([ + page.waitForEvent('download'), // Wait for the download event + page.getByLabel('Export Model Data').click(), // Dropdown + page.locator('div[role="menuitem"]:has-text("Export as XLSX")').click()// Export + ]); + + const filePathXLSX = await downloadXSLX.path(); + if (filePathXLSX) { + console.log(`File successfully downloaded to: ${filePathXLSX}`); + } else { + console.error('Download failed.'); + } + + // Select "Generate Metrics with AI", + await Promise.all([ + waitForFileNavEntry(page, "/metrics/sales_model_metrics.yaml", false), //set true? + page.getByRole('button', { name: 'Generate metrics view' }).click(), + ]); + + // Return to source and check Go to for both models. + await page.locator('span:has-text("sales_model.sql")').click(); + + await page.getByRole('button', { name: 'Go to metrics view' }).click(); + + await Promise.all([ + waitForFileNavEntry(page, "/metrics/sales_model_metrics_1.yaml", false), //set true? + page.getByText('Create metrics view').click(), + ]); + + await expect(page.getByRole('link', { name: 'sales_model_metrics.yaml' })).toBeVisible(); + await expect(page.getByRole('link', { name: 'sales_model_metrics_1.yaml' })).toBeVisible(); + + // Delete a metrics vie and rename another + await page.locator('span:has-text("sales_model_metrics.yaml")').hover(); + await actionUsingMenu(page, "/sales_model_metrics.yaml", "Delete") + + await renameFileUsingMenu(page, '/metrics/sales_model_metrics_1.yaml', 'random_metrics.yaml') + + // Check the model and metrics are still linked + await page.locator('span:has-text("sales_model.sql")').click(); + await page.getByRole('button', { name: 'Go to metrics view' }).click(); + await page.locator('div[role="menuitem"]:has-text("Create metrics view")').waitFor(); + await expect(page.getByRole('menuitem', { name: 'random_metrics', exact: true })).toBeVisible(); + await page.getByRole('menuitem', { name: 'random_metrics', exact: true }).click(); + + // Can add further testing like renaming files and creating metrics from button to see if number is correct. + + await page.locator('span:has-text("random_metrics.yaml")').hover(); + await actionUsingMenu(page, "/random_metrics.yaml", "Delete") + + // Check the UI has returned to Generate metrics view with AI + await page.locator('span:has-text("sales_model.sql")').click(); + await expect(page.getByRole('button', { name: 'Generate metrics view' })).toBeVisible(); + + }); +}); \ No newline at end of file diff --git a/web-local/tests/UI/check-source-ui-buttons.spec.ts b/web-local/tests/UI/check-source-ui-buttons.spec.ts new file mode 100644 index 00000000000..ff7255bbb36 --- /dev/null +++ b/web-local/tests/UI/check-source-ui-buttons.spec.ts @@ -0,0 +1,116 @@ +import { test, expect } from '@playwright/test'; +import { test as RillTest } from '../utils/test'; +import { cloud, waitForTable } from '../utils/sourceHelpers'; +import { actionUsingMenu, renameFileUsingMenu, checkExistInConnector } from "../utils/commonHelpers"; + +// Testing functionality of all the buttons in the source page. +// Refresh, "Create Model", "Go to "(after creating model), panels + + +test.describe('Check Source UI buttons.', () => { + RillTest('Reading Source into Rill from GCS', async ({ page }) => { + console.log('Testing cloud sales data ingestion...'); + await Promise.all([ + waitForTable(page, '/sources/sales.yaml', [ + 'sale_date', + 'sale_id', + 'duration_ms', + 'customer_id', + 'sales_amount_usd', + 'products', + 'discounts', + 'region', + 'is_online', + ]), + cloud(page, 'sales.csv', 'gcs'), + ]); + console.log('Sales table validated...'); + + // CHECK CONNECTORS for SOURCE (table name dynamic so wildcard) + await checkExistInConnector(page, 'duckdb', 'main_db', 'sales') + + // CHECKING BUTTONS + + //Close File Explore Sidebar + await page.locator('span[aria-label="Close sidebar"]').click(); + // Assert that the class changes + const sidebarClose = page.locator('.sidebar.svelte-5nrsv4'); + await expect(sidebarClose).toHaveClass('sidebar svelte-5nrsv4 hide'); + + + await page.locator('span[aria-label="Show sidebar"]').click(); + // Assert that the class changes + const sidebarOpen = page.locator('.sidebar.svelte-5nrsv4'); + await expect(sidebarOpen).toHaveClass('sidebar svelte-5nrsv4'); + + + // checking the refresh button + await Promise.all([ + page.locator('button[aria-label="Refresh"]').click(), + expect(page.getByText('Ingesting source sales').first()).toBeVisible() // Test will fail if the text is not visible + ]); + // checking the panels , + await page.getByRole('button', { name: 'Toggle table visibility' }).click(); // #6308 + const resultsPreviewTableClose = await page.locator('[aria-label="Results Preview Table"]'); // #6316 + await expect(resultsPreviewTableClose).toBeHidden(); + + await page.getByRole('button', { name: 'Toggle table visibility' }).click(); // #6308 + const resultsPreviewTableOpen = await page.locator('[aria-label="Results Preview Table"]'); // #6316 + await expect(resultsPreviewTableOpen).toBeVisible(); + + await page.getByRole('button', { name: 'Toggle inspector visibility' }).click(); // #6308 + const inspectorPanelClose = await page.locator('[aria-label="Inspector Panel"]'); // #6316 + await expect(inspectorPanelClose).toBeHidden(); + + await page.getByRole('button', { name: 'Toggle inspector visibility' }).click(); // #6308 + const inspectorPanelOpen = await page.locator('[aria-label="Inspector Panel"]'); // #6316 + await expect(inspectorPanelOpen).toBeVisible(); + + // Create Model! + console.log("Creating Create Model Button...") + await Promise.all([ + // waitForFileNavEntry(page, "/files/models/sales_model.sql", false), //set true? + page.getByRole('button', { name: 'Create model' }).click() + ]); + + + // Return to Source, Select Go to. -> create model, check that its suffixed "_1" + await page.locator('span:has-text("sales.yaml")').click(); + + // waitForFileNavEntry(page, "/models/sales_model_1.sql", false), //set true? + await page.getByRole('button', { name: 'Go to' }).click(); + await page.locator('div[role="menuitem"]:has-text("Create model")').waitFor(); + await page.locator('div[role="menuitem"]:has-text("Create model")').click(); + + + // Return to source and check Go to for both models. + await page.locator('span:has-text("sales.yaml")').click(); + await page.getByRole('button', { name: 'Go to' }).click(); + await page.locator('div[role="menuitem"]:has-text("Create model")').waitFor(); + await expect(page.locator('a[role="menuitem"][href="/files/models/sales_model.sql"]')).toBeVisible(); + await expect(page.locator('a[role="menuitem"][href="/files/models/sales_model_1.sql"]')).toBeVisible(); + + // Delete and rename models and confirm back to Create model + await page.locator('span:has-text("sales_model.sql")').hover(); + await actionUsingMenu(page, "/sales_model.sql", "Delete") + + await renameFileUsingMenu(page, '/models/sales_model_1.sql', 'random_model.sql') + + // Check the source and model are still linked + await page.locator('span:has-text("sales.yaml")').click(); + await page.getByRole('button', { name: 'Go to' }).click(); + await page.locator('div[role="menuitem"]:has-text("Create model")').waitFor(); + await expect(page.locator('a[role="menuitem"][href="/files/models/random_model.sql"]')).toBeVisible(); + await page.locator('a[role="menuitem"][href="/files/models/random_model.sql"]').click(); + + + // Delete new model + await page.locator('span:has-text("random_model.sql")').hover(); + await actionUsingMenu(page, "/random_model.sql", "Delete") + + // Check the UI has returned to Create Model + await page.locator('span:has-text("sales.yaml")').click(); + await expect(page.getByRole('button', { name: 'Create model' })).toBeVisible(); + + }); +}); \ No newline at end of file