diff --git a/kokoro/gcp_ubuntu_docker/kokoro_build.sh b/kokoro/gcp_ubuntu_docker/kokoro_build.sh index e3fc356f..91b1a34f 100644 --- a/kokoro/gcp_ubuntu_docker/kokoro_build.sh +++ b/kokoro/gcp_ubuntu_docker/kokoro_build.sh @@ -48,7 +48,7 @@ jupyter server extension enable dataproc_jupyter_plugin cd ./ui-tests jlpm install jlpm playwright install -PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-results-latest/sponge_log.xml jlpm playwright test --reporter=junit --output="test-results-latest" +PLAYWRIGHT_JUNIT_OUTPUT_NAME=test-results-latest/sponge_log.xml TEST_TAG=Sanity jlpm playwright test --reporter=junit --output="test-results-latest" deactivate # Test 3.6.6 diff --git a/src/sessions/sessionDetails.tsx b/src/sessions/sessionDetails.tsx index 26409255..a651d779 100644 --- a/src/sessions/sessionDetails.tsx +++ b/src/sessions/sessionDetails.tsx @@ -467,7 +467,7 @@ function SessionDetails({
-
+
{DATAPROC_CLUSTER_LABEL}
diff --git a/src/sessions/sessionTemplate.tsx b/src/sessions/sessionTemplate.tsx index a20bd491..6bc458e8 100644 --- a/src/sessions/sessionTemplate.tsx +++ b/src/sessions/sessionTemplate.tsx @@ -37,7 +37,7 @@ const [detailedSessionView,setDetailedSessionView] = useState(true); useEffect(() => { }); return ( -
+
{detailedSessionView && ( { - const issues = await page.request.get( - 'http://localhost:8888/dataproc-plugin/settings' - ); - expect(issues.ok()).toBeTruthy(); - const response: any = await issues.json(); - expect(response.enable_bigquery_integration).toBe(true); - - const tabExists = await page.isVisible( - 'role=tab[name="Dataset Explorer - BigQuery"]' - ); - - if (tabExists) { +test.describe('bigquery-dataset-explorer', () => { + test('Sanity : Dataset Explorer', async ({ page, request }) => { + // Fetch Dataproc plugin settings + const issues = await page.request.get('http://localhost:8888/dataproc-plugin/settings'); + expect(issues.ok()).toBeTruthy(); + + // Parse response and check BigQuery integration is enabled + const response = await issues.json(); + expect(response.enable_bigquery_integration).toBe(true); + + // Check if the "Dataset Explorer - BigQuery" tab is visible + const tabExists = await page.isVisible('role=tab[name="Dataset Explorer - BigQuery"]'); + + if (tabExists) { + // Click on the "Dataset Explorer - BigQuery" tab + await page.getByRole('tab', { name: 'Dataset Explorer - BigQuery' }).click(); + + // Expand and navigate through the dataset tree + await page.waitForSelector('div[role="treeitem"].caret-icon.down'); + await page.locator('div[role="treeitem"].caret-icon.down').nth(1).click(); + await page.getByTestId('loader').waitFor({ state: "detached" }); + await page.locator('div[role="treeitem"].caret-icon.down').nth(1).click(); + await page.getByTestId('loader').waitFor({ state: "detached" }); + await page.locator('div[role="treeitem"][aria-level="2"]').first().click(); + + // Wait for the dataset details to load + await page.getByText('Loading dataset details').waitFor({ state: "hidden" }); + + // Check dataset details are visible + await expect(page.getByText('Dataset info', { exact: true })).toBeVisible(); + const datasetRowHeaders = [ + 'Dataset ID', 'Created', 'Default table expiration', + 'Last modified', 'Data location', 'Description', 'Default collation', + 'Default rounding mode', 'Time travel window', 'Storage billing model', 'Case insensitive' + ]; + for (const header of datasetRowHeaders) { + await expect(page.getByRole('cell', { name: header, exact: true })).toBeVisible(); + } + + // Click on the first table + await page.locator('div[role="treeitem"][aria-level="3"]').first().click(); + + // Wait for the table details to load + await page.getByText('Loading table details').waitFor({ state: "hidden" }); + + // Check table details are visible + await expect(page.getByText('Table info')).toBeVisible(); + const tableHeaders = [ + 'Table ID', 'Created', 'Last modified', 'Table expiration', + 'Data location', 'Default collation', 'Default rounding mode', 'Description', 'Case insensitive' + ]; + for (const header of tableHeaders) { + await expect(page.getByRole('cell', { name: header, exact: true })).toBeVisible(); + } + + // Click on the Schema tab and check all schema fields are visible + await page.getByText('Schema', { exact: true }).click(); + await expect(page.getByText('Schema').nth(2)).toBeVisible(); + const schemaHeaders = [ + 'Field name', 'Type', 'Mode', 'Key', 'Collation', 'Default Value', + 'Policy Tags', 'Data Policies', 'Description' + ]; + for (const header of schemaHeaders) { + await expect(page.getByRole('columnheader', { name: header, exact: true })).toBeVisible(); + } + + // Click on the Preview tab and check if data is present + await page.getByText('Preview', { exact: true }).click(); + + // Wait for the preview data to load + await page.getByText('Loading Preview Data').waitFor({ state: "hidden" }); + + // Check if data is present in the preview table + const dataExists = await page.locator('table.clusters-list-table').isVisible(); + if (dataExists) { + const rowCount = await page.locator('table.clusters-list-table tr').count(); + expect(rowCount).toBeGreaterThan(0); + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + + } else { + // If the tab doesn't exist, verify BigQuery integration is disabled + expect(response.enable_bigquery_integration).toBe(false); + } + }); + + test('Can verify search field and refresh button', async ({ page, request }) => { + // Fetch Dataproc plugin settings + const issues = await page.request.get('http://localhost:8888/dataproc-plugin/settings'); + expect(issues.ok()).toBeTruthy(); + + // Parse response and check BigQuery integration is enabled + const response = await issues.json(); expect(response.enable_bigquery_integration).toBe(true); - await page - .getByRole('tab', { name: 'Dataset Explorer - BigQuery' }) - .click(); - - // Wait for the Dataset projects to populate. - await page.waitForSelector('div[role="treeitem"][aria-level="1"]'); - await page.waitForSelector('div[role="treeitem"].caret-icon.down'); - - // Expand the first dataset project. This should always be the `bigquery-public-data` one. - await page.locator('div[role="treeitem"].caret-icon.down').nth(0).click(); - - // Wait for the first dataset to be displayed, and then expand it. - await page.waitForSelector('div[role="treeitem"][aria-level="2"]'); - await page.waitForSelector('div[role="treeitem"].caret-icon.down'); - await page.locator('div[role="treeitem"].caret-icon.down').nth(0).click(); - - // Click on the first table displayed. - await page.locator('div[role="treeitem"][aria-level="3"]').first().click(); - await page.getByText('Schema', { exact: true }).click(); - await page.getByText('Preview', { exact: true }).click(); - } else { - expect(response.enable_bigquery_integration).toBe(false); - } -}); + + // Check if the "Dataset Explorer - BigQuery" tab is visible + const tabExists = await page.isVisible('role=tab[name="Dataset Explorer - BigQuery"]'); + + if (tabExists) { + // Click on the "Dataset Explorer - BigQuery" tab + await page.getByRole('tab', { name: 'Dataset Explorer - BigQuery' }).click(); + + await page.waitForSelector('div[role="treeitem"].caret-icon.down'); + + // Check austin_311 is not visible before search + await expect(page.locator('[role="treeitem"][title="austin_311"]')).not.toBeVisible(); + + await page.getByPlaceholder('Enter your keyword to search').first().click(); + await page.getByPlaceholder('Enter your keyword to search').first().fill('austin_311'); + + await page.locator('[aria-label="Loading Spinner"]').waitFor({ state: "detached" }); + + await page.waitForTimeout(5000); + + // Check austin_311 is visible after search + await expect(page.locator('[role="treeitem"][title="austin_311"]')).toBeVisible(); + + // Click on refresh button and check austin_311 is not visible + await page.locator('.dataset-explorer-refresh > .icon-white > svg').first().click(); + await expect(page.locator('[role="treeitem"][title="austin_311"]')).not.toBeVisible(); + } + }); +}); \ No newline at end of file diff --git a/ui-tests/tests/check_clusters_jobs.spec.ts b/ui-tests/tests/check_clusters_jobs.spec.ts new file mode 100644 index 00000000..23bf8398 --- /dev/null +++ b/ui-tests/tests/check_clusters_jobs.spec.ts @@ -0,0 +1,764 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test, galata } from '@jupyterlab/galata'; +import { Locator } from '@playwright/test'; + +test.describe('Clusters tests', () => { + + // Set a common timeout for all tests + const timeout = 5 * 60 * 1000; + + // Function to navigate to Clusters page + async function navigateToClusters(page) { + await page.locator('//*[@data-category="Google Cloud Resources" and @title="Clusters"]').click(); + await page.getByText('Loading Clusters').waitFor({ state: "detached" }); + } + + test('Sanity: Can verify tabs and fields on the page', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Clusters page + await navigateToClusters(page); + + // Verify Clusters and Jobs tabs + await expect(page.getByRole('tabpanel').getByText('Clusters', { exact: true })).toBeVisible(); + await expect(page.getByRole('tabpanel').getByText('Jobs', { exact: true })).toBeVisible(); + + // Verify Create Cluster button + await expect(page.getByRole('button', { name: 'Create cluster' })).toBeVisible(); + + // Check if cluster table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + // Check search field is present + await expect(page.getByPlaceholder('Filter Table')).toBeVisible(); + + // Check list of clusters are displayed + const rowCount = await page.locator('//table[@class="clusters-list-table"]//tr').count(); + expect(rowCount).toBeGreaterThan(0); + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Sanity: Can verify clusters table headers', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Clusters page + await navigateToClusters(page); + + // Check clusters table headers if table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + const headers = [ + 'Name', 'Status', 'Cluster image name', 'Region', 'Zone', + 'Total worker nodes', 'Scheduled deletion', 'Actions' + ]; + for (const header of headers) { + await expect(page.getByRole('columnheader', { name: header, exact: true })).toBeVisible(); + } + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Sanity: Can click on Cluster and validate the cluster details', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Clusters page + await navigateToClusters(page); + + // Check if clusters table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + const clusterLocator = page.locator('//*[@class="cluster-name"]').nth(1); + const clusterName = await clusterLocator.innerText(); + await clusterLocator.click(); + await page.getByText('Loading Cluster Details').waitFor({ state: "detached" }); + + // Verify cluster details elements + await expect(page.getByLabel('back-arrow-icon')).toBeVisible(); + await expect(page.getByText('Cluster details')).toBeVisible(); + await expect(page.getByRole('button', { name: 'START', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'STOP', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'VIEW CLOUD LOGS', exact: true })).toBeVisible(); + await expect(page.getByLabel('Clusters').getByText('Name', { exact: true })).toBeVisible(); + await expect(page.locator(`//*[@class="cluster-details-value" and text()="${clusterName}"]`)).toBeVisible(); + await expect(page.getByText('Cluster UUID', { exact: true })).toBeVisible(); + await expect(page.getByText('Type', { exact: true })).toBeVisible(); + await expect(page.getByText('Status', { exact: true })).toBeVisible(); + await expect(page.getByText('Jobs', { exact: true })).toBeVisible(); + + // Wait for jobs to load and verify elements + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + await expect(page.getByRole('button', { name: 'SUBMIT JOB', exact: true })).toBeVisible(); + + const noRows = await page.getByText('No rows to display').isVisible(); + if (noRows) { + await expect(page.getByText('No rows to display')).toBeVisible(); + } else { + const jobHeaders = [ + 'Job ID', 'Status', 'Region', 'Type', 'Start time', + 'Elapsed time', 'Labels', 'Actions' + ]; + for (const header of jobHeaders) { + await expect(page.getByRole('columnheader', { name: header, exact: true })).toBeVisible(); + } + } + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test.skip('Can start, restart, and stop the cluster', async ({ page }) => { + test.setTimeout(timeout); + + // Click on Google Cloud Resources - Clusters card + await page.locator('//*[@data-category="Google Cloud Resources" and @title="Clusters"]').click(); + + // Wait until the Clusters page loads + await page.getByText('Loading Clusters').waitFor({ state: "detached" }); + + const parentLocator = page.locator('//tr[@class="cluster-list-data-parent"]'); + const noOfClusters = await parentLocator.count(); + + for (let i = 0; i < noOfClusters; i++) { + // Check if the current cluster's "Start Cluster" button is enabled + const startEnabled = await parentLocator.nth(i).locator('//div[@title="Start Cluster"]').isEnabled(); + if (startEnabled) { + // Start the cluster + await startCluster(parentLocator.nth(i)); + + // Restart the cluster + await restartCluster(parentLocator.nth(i)); + + // Stop the cluster + await stopCluster(parentLocator.nth(i)); + break; + } + } + + // Function to start a cluster + async function startCluster(clusterLocator) { + await clusterLocator.locator('//div[@title="Start Cluster"]').click(); + await clusterLocator.getByText('stopped').waitFor({ state: "detached" }); + await clusterLocator.getByText('starting').waitFor({ state: "detached" }); + await expect(clusterLocator.getByText('running')).toBeVisible(); + } + + // Function to restart a cluster + async function restartCluster(clusterLocator) { + await expect(clusterLocator.locator('//div[@title="Restart Cluster"]')).toBeEnabled(); + await clusterLocator.locator('//div[@title="Restart Cluster"]').click(); + await clusterLocator.getByText('running').waitFor({ state: "detached" }); + await expect(clusterLocator.getByText('stopping')).toBeVisible(); + await clusterLocator.getByText('stopping').waitFor({ state: "detached" }); + await expect(clusterLocator.getByText('stopped')).toBeVisible(); + await clusterLocator.getByText('stopped').waitFor({ state: "detached" }); + await expect(clusterLocator.getByText('starting')).toBeVisible(); + await clusterLocator.getByText('starting').waitFor({ state: "detached" }); + await expect(clusterLocator.getByText('running')).toBeVisible(); + } + + // Function to stop a cluster + async function stopCluster(clusterLocator) { + await expect(clusterLocator.locator('//div[@title="Stop Cluster"]')).toBeEnabled(); + await clusterLocator.locator('//div[@title="Stop Cluster"]').click(); + await clusterLocator.getByText('running').waitFor({ state: "detached" }); + await clusterLocator.getByText('stopping').waitFor({ state: "detached" }); + await expect(clusterLocator.getByText('stopped')).toBeVisible(); + } + }); + + test('Can verify jobs table headers', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Clusters page + await navigateToClusters(page); + + // Click on Jobs tab + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + + // Verify Jobs tab elements and table headers + await expect(page.getByRole('button', { name: 'SUBMIT JOB', exact: true })).toBeVisible(); + + const noRows = await page.getByText('No rows to display').isVisible(); + if (noRows) { + await expect(page.getByText('No rows to display')).toBeVisible(); + } else { + await expect(page.locator('//table[@class="clusters-list-table"]')).toBeVisible(); + const jobHeaders = [ + 'Job ID', 'Status', 'Region', 'Type', 'Start time', + 'Elapsed time', 'Labels', 'Actions' + ]; + for (const header of jobHeaders) { + await expect(page.getByRole('columnheader', { name: header, exact: true })).toBeVisible(); + } + } + }); + + test('Can click on Job ID and validate the details', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Clusters page and switch to Jobs tab + await navigateToClusters(page); + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + + // Check if jobs table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + // Click on first Job id + const firstJobLocator = page.locator('//*[@class="cluster-name"]').nth(1); + const jobName = await firstJobLocator.innerText(); + await firstJobLocator.click(); + + // Wait for Job Details to load + await page.getByText('Loading Job Details').waitFor({ state: "detached" }); + + + // Verify the Job details + await expect(page.locator('//div[@class="back-arrow-icon"]')).toBeVisible(); + await expect(page.getByText('Job details')).toBeVisible(); + await expect(page.getByRole('button', { name: 'CLONE', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'STOP', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'DELETE', exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'VIEW SPARK LOGS', exact: true })).toBeVisible(); + await expect(page.getByText('Job ID')).toBeVisible(); + await expect(page.locator(`//*[@class="cluster-details-value" and text()="${jobName}"]`).first()).toBeVisible(); + await expect(page.getByText('Job UUID', { exact: true })).toBeVisible(); + await expect(page.getByText('Type', { exact: true })).toBeVisible(); + await expect(page.getByText('Status', { exact: true }).first()).toBeVisible(); + await expect(page.getByText('Configuration', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'EDIT' })).toBeVisible(); + await expect(page.getByText('Start time:')).toBeVisible(); + await expect(page.getByText('Elapsed time:')).toBeVisible(); + await expect(page.getByText('Status').nth(1)).toBeVisible(); + await expect(page.getByText('Region')).toBeVisible(); + await expect(page.getByText('Cluster', { exact: true })).toBeVisible(); + await expect(page.getByText('Job type')).toBeVisible(); + await expect(page.getByText('Labels', { exact: true })).toBeVisible(); + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Can verify clone a job from job details page', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Clusters page and switch to Jobs tab + await navigateToClusters(page); + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + + // Check if jobs table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + // Click on first Job id + const firstJobLocator = page.locator('//*[@class="cluster-name"]').nth(1); + const jobName = await firstJobLocator.innerText(); + await firstJobLocator.click(); + + // Wait for Job Details to load + await page.getByText('Loading Job Details').waitFor({ state: "detached" }); + + // Click on clone button + await page.getByRole('button', { name: 'CLONE' }).click(); + + // Capture job id and click on Submit button + const jobId = await page.getByLabel('Job ID*').inputValue(); + await page.getByRole('button', { name: 'SUBMIT' }).click(); + + await page.waitForTimeout(5000); + await expect(page.getByText(`Job ${jobId} successfully submitted`)).toBeVisible(); + + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Can verify clone a job from job listing page', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Clusters page and switch to Jobs tab + await navigateToClusters(page); + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + + // Check if jobs table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + + // Click on clone button + await page.getByRole('button', { name: 'CLONE' }).first().click(); + + // Capture job id and click on Submit button + const jobId = await page.getByLabel('Job ID*').inputValue(); + await page.getByRole('button', { name: 'SUBMIT' }).click(); + + await page.waitForTimeout(5000); + await expect(page.getByText(`Job ${jobId} successfully submitted`)).toBeVisible(); + + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test.skip('Can delete a job using actions delete icon', async ({ page }) => { // Dut to atribute aria-disabled="true", automation script not able to perform delete action + test.setTimeout(5 * 60 * 1000); + + // Navigate to Clusters page and switch to Jobs tab + await navigateToClusters(page); + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + + // Check if jobs table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + // Get the first job ID and click on delete option + const jobhId = await page.getByRole('cell').first().innerText(); + await page.getByTitle('Delete Job').first().click(); + + // Confirm deletion and verify success message + await expect(page.getByText(`This will delete ${jobhId} and cannot be undone.`)).toBeVisible(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Job ${jobhId} deleted successfully`)).toBeVisible(); + + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + + }); + + test('Can delete a job from job details page', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Clusters page and switch to Jobs tab + await navigateToClusters(page); + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + + // Check if jobs table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + // Get the first job ID and delete it + const jobhId = await page.getByRole('cell').first().innerText(); + await page.getByRole('cell').first().click(); + + // Click the delete button, confirm deletion, and verify success message + await page.getByRole('button', { name: 'DELETE', exact: true }).click(); + await expect(page.getByText(`This will delete ${jobhId} and cannot be undone.`)).toBeVisible(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Job ${jobhId} deleted successfully`)).toBeVisible(); + + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + // Function to select the first cluster if available + async function selectCluster(page) { + await page.getByRole('combobox', { name: 'Cluster*' }).click(); + const noOfClusters = await page.getByRole('option').count(); + if (noOfClusters >= 1) { + await page.getByRole('option').first().click(); + return true; // Cluster available + } else { + expect(noOfClusters).toBe(0); + return false; // No clusters available + } + } + + // Function to fill the Labels section + async function fillLabels(page) { + await expect(page.getByText('Labels')).toBeVisible(); + await expect(page.locator('//label[text()="Key 1*"]/following-sibling::div/input[@value="client"]')).toBeVisible(); + await expect(page.locator('//label[text()="Value 1"]/following-sibling::div/input[@value="dataproc-jupyter-plugin"]')).toBeVisible(); + await page.getByRole('button', { name: 'ADD LABEL' }).click(); + await page.getByLabel('Key 2*').fill('goog-dataproc-location'); + await page.getByLabel('Value 2').fill('us-central1'); + } + + // Function to submit the job and validate submission + async function submitJobAndValidate(page, jobId) { + await page.getByRole('button', { name: 'SUBMIT' }).click(); + await page.waitForTimeout(5000); // Wait for submission to complete + await expect(page.getByText(`Job ${jobId} successfully submitted`)).toBeVisible(); + } + + test('Can validate fields and create a Spark job', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to the Jobs tab + await navigateToClusters(page); + + // Check if cluster table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + + if (tableExists) { + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + + // Click the SUBMIT JOB button + await page.waitForTimeout(5000); + await page.getByRole('button', { name: 'SUBMIT JOB', exact: true }).click(); + + // select the first available clusters + await page.getByRole('combobox', { name: 'Cluster*' }).click(); + await page.getByRole('option').first().click(); + + // Capture Job ID and verify default job type + const jobId = await page.getByLabel('Job ID*').inputValue(); + const jobType = await page.getByLabel('Job type*').inputValue(); + expect(jobType).toBe('Spark'); + + // Verify fields + await expect(page.getByLabel('Main class or jar*')).toBeVisible(); + await page.getByLabel('Main class or jar*').fill('gs://dataproc-extension/DO NOT DELETE/helloworld-2.0.jar'); + await expect(page.getByLabel('Jar files')).toBeVisible(); + await expect(page.getByLabel('Files', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Archive files')).toBeVisible(); + await expect(page.getByLabel('Arguments')).toBeVisible(); + await expect(page.getByLabel('Max restarts per hour')).toBeVisible(); + + // Fill the Labels section + await fillLabels(page); + + // Submit the job and validate + await submitJobAndValidate(page, jobId); + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Can validate fields and create a SparkR job', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to the Jobs tab + await navigateToClusters(page); + + // Check if cluster table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + + if (tableExists) { + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + + // Click the SUBMIT JOB button + await page.waitForTimeout(5000); + await page.getByRole('button', { name: 'SUBMIT JOB', exact: true }).click(); + + // select the first available clusters + await page.getByRole('combobox', { name: 'Cluster*' }).click(); + await page.getByRole('option').first().click(); + + // Capture Job ID + const jobId = await page.getByLabel('Job ID*').inputValue(); + + // Change job type to SparkR + await page.getByLabel('Open', { exact: true }).nth(1).click(); + await page.getByRole('option', { name: 'SparkR' }).click(); + + // Verify fields and fill necessary inputs + await expect(page.getByLabel('Main R file*')).toBeVisible(); + await page.getByLabel('Main R file*').fill('gs://dataproc-extension/DO NOT DELETE/helloworld.r'); + await expect(page.getByLabel('Files', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Arguments')).toBeVisible(); + await expect(page.getByLabel('Max restarts per hour')).toBeVisible(); + + // Fill the Labels section + await fillLabels(page); + + // Submit the job and validate + await submitJobAndValidate(page, jobId); + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Can validate fields and create a SparkSql job', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to the Jobs tab + await navigateToClusters(page); + + // Check if cluster table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + + if (tableExists) { + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + + // Click the SUBMIT JOB button + await page.waitForTimeout(5000); + await page.getByRole('button', { name: 'SUBMIT JOB', exact: true }).click(); + + // select the first available clusters + await page.getByRole('combobox', { name: 'Cluster*' }).click(); + await page.getByRole('option').first().click(); + + // Capture Job ID + const jobId = await page.getByLabel('Job ID*').inputValue(); + + // Change job type to SparkSql + await page.getByLabel('Open', { exact: true }).nth(1).click(); + await page.getByRole('option', { name: 'SparkSql' }).click(); + + await expect(page.getByLabel('Jar files')).toBeVisible(); + await expect(page.getByLabel('Max restarts per hour')).toBeVisible(); + + // Select query source type and enter query file + await page.getByRole('combobox', { name: 'Query source type*' }).click(); + await page.getByRole('option', { name: 'Query file' }).click(); + await page.getByLabel('Query file*').fill('gs://dataproc-extension/DO NOT DELETE/sampleSQL.sql'); + + // Fill the Labels section + await fillLabels(page); + + // Submit the job and validate + await submitJobAndValidate(page, jobId); + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Can validate fields and create a PySpark job', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to the Jobs tab + await navigateToClusters(page); + + // Check if cluster table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + + if (tableExists) { + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + + // Click the SUBMIT JOB button + await page.waitForTimeout(5000); + await page.getByRole('button', { name: 'SUBMIT JOB', exact: true }).click(); + + // select the first available clusters + await page.getByRole('combobox', { name: 'Cluster*' }).click(); + await page.getByRole('option').first().click(); + + // Capture Job ID + const jobId = await page.getByLabel('Job ID*').inputValue(); + + // Change job type to PySpark + await page.getByLabel('Open', { exact: true }).nth(1).click(); + await page.getByRole('option', { name: 'PySpark' }).click(); + + // Verify fields + await expect(page.getByLabel('Additional python files')).toBeVisible(); + await expect(page.getByLabel('Jar files')).toBeVisible(); + await expect(page.getByLabel('Files', { exact: true })).toBeVisible(); + await expect(page.getByLabel('Archive files')).toBeVisible(); + await expect(page.getByLabel('Arguments')).toBeVisible(); + await expect(page.getByLabel('Max restarts per hour')).toBeVisible(); + + // Fill Main Python file and Labels + await expect(page.getByLabel('Main Python file*')).toBeVisible(); + await page.getByLabel('Main Python file*').fill('gs://dataproc-extension/DO NOT DELETE/helloworld1.py'); + await fillLabels(page); + + // Submit the job and validate + await submitJobAndValidate(page, jobId); + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Sanity: Can perform field validations', async ({ page }) => { + test.setTimeout(timeout); // Set the test timeout + + // Navigate to the Jobs tab + await navigateToClusters(page); + + // Check if cluster table data is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + + if (tableExists) { + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + + // Click the SUBMIT JOB button + await page.waitForTimeout(5000); + await page.getByRole('button', { name: 'SUBMIT JOB', exact: true }).click(); + + // Job ID field validation: Empty field should show error + await page.getByLabel('Job ID*').fill(''); + await expect(page.getByText('ID is required')).toBeVisible(); // Error should appear for empty Job ID + + // Fill valid Job ID and check if error is hidden + await page.getByLabel('Job ID*').fill('testJob'); + await expect(page.getByText('ID is required')).toBeHidden(); + + // Main class or jar validation + await page.getByLabel('Main class or jar*').fill('test'); + await page.getByText('ClusterChoose a cluster to').click(); // Trigger validation by clicking outside + + // Clear the field and expect an error message + await page.getByLabel('Main class or jar*').fill(''); + await page.getByLabel('Jar files').click(); // Click to trigger validation + await expect(page.getByText('Main class or jar is required')).toBeVisible(); // Error should appear + + // Refill the field and check if the error is hidden + await page.getByLabel('Main class or jar*').fill('test'); + await expect(page.getByText('Main class or jar is required')).toBeHidden(); + + // Jar files validation + await page.getByLabel('Jar files').fill('test'); + await page.getByText('ClusterChoose a cluster to').click(); // Trigger validation + await expect(page.locator("//*[contains(text(),'All files must include a valid scheme prefix: ')]")).toBeVisible(); + + // Clear the invalid Jar files and check if the error is hidden + await page.getByRole('button', { name: 'Clear' }).click(); + await expect(page.locator("//*[contains(text(),'All files must include a valid scheme prefix: ')]")).toBeHidden(); + + // Files validation + await page.getByLabel('Files', { exact: true }).fill('test'); + await page.getByText('ClusterChoose a cluster to').click(); + await expect(page.locator("//*[contains(text(),'All files must include a valid scheme prefix: ')]")).toBeVisible(); + + // Clear and revalidate the Files field + await page.getByRole('button', { name: 'Clear' }).click(); + await expect(page.locator("//*[contains(text(),'All files must include a valid scheme prefix: ')]")).toBeHidden(); + + // Archive files validation + await page.getByLabel('Archive files').fill('test'); + await page.getByText('ClusterChoose a cluster to').click(); + await expect(page.locator("//*[contains(text(),'All files must include a valid scheme prefix: ')]")).toBeVisible(); + + // Clear and check if the error is hidden + await page.getByRole('button', { name: 'Clear' }).click(); + await expect(page.locator("//*[contains(text(),'All files must include a valid scheme prefix: ')]")).toBeHidden(); + + // Add property validation: Empty key should show an error + await page.getByRole('button', { name: 'ADD PROPERTY' }).click(); + await expect(page.getByText('key is required')).toBeVisible(); // Key field should be required + + // Check if the ADD PROPERTY button is disabled + let isDisabled = await page.getByRole('button', { name: 'ADD PROPERTY' }).getAttribute('class'); + expect(isDisabled).toContain('disabled'); // Button should be disabled initially + + // Fill the key field and ensure the error is hidden + await page.getByLabel('Key 1*').first().fill('key'); + await expect(page.getByText('key is required')).toBeHidden(); // Error should disappear after entering the key + + // Click on the value field and ensure the ADD PROPERTY button is enabled + await page.getByLabel('Value').first().click(); + isDisabled = await page.getByRole('button', { name: 'ADD PROPERTY' }).getAttribute('class'); + expect(isDisabled).not.toContain('disabled'); // Button should now be enabled + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Sanity: Check pagination in serverless batches tab', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToClusters(page); + await page.getByRole('tabpanel').getByText('Jobs', { exact: true }).click(); + await page.getByText('Loading Jobs').waitFor({ state: "detached" }); + + // Check if pagination is visible on the page + const paginationLocator = page.locator('//div[@class="pagination-parent-view"]'); + const isPaginationVisible = await paginationLocator.isVisible(); + + // Function to validate row count + const validateRowCount = async (tableRowsLocator: Locator, expectedCount: number) => { + const rowCount = await tableRowsLocator.count(); + expect(rowCount).toBeLessThanOrEqual(expectedCount); + }; + + // Function to change rows per page and validate + const changeRowsPerPageAndValidate = async (paginationLocator: Locator, tableRowsLocator: Locator, newRowsPerPage: number) => { + let currentRowsPerPage = await paginationLocator.locator('input').getAttribute('value'); + await page.getByText(currentRowsPerPage!, { exact: true }).click(); + await page.getByRole('option', { name: String(newRowsPerPage) }).click(); + await page.waitForTimeout(2000); + await validateRowCount(tableRowsLocator, newRowsPerPage); + }; + + // Function to navigate and validate row count + const navigateAndValidate = async (buttonLocator: Locator, tableRowsLocator: Locator, expectedCount: number) => { + await buttonLocator.click(); + await page.waitForTimeout(2000); + await validateRowCount(tableRowsLocator, expectedCount); + }; + + // Function to validate the range + const validateRange = async (pageRange: Locator, expectedStart: number, expectedEnd: number) => { + const rangeText = await pageRange.textContent(); + const [currentRange, totalCount] = rangeText!.split(" of "); + const [start, end] = currentRange.split(" - ").map(Number); + expect(start).toBe(expectedStart); + expect(end).toBeLessThanOrEqual(expectedEnd); + }; + + if (isPaginationVisible) { + const tableRowsLocator = page.locator('//tbody[@class="clusters-table-body"]/tr[@role="row"]'); + + // Verify the default row count and selected pagination number + await validateRowCount(tableRowsLocator, 50); + let currentRowsPerPage = await paginationLocator.locator('input').getAttribute('value'); + expect(currentRowsPerPage).toBe('50'); + + // Change rows per page to 100 and validate + await changeRowsPerPageAndValidate(paginationLocator, tableRowsLocator, 100); + + // Change rows per page to 200 and validate + await changeRowsPerPageAndValidate(paginationLocator, tableRowsLocator, 200); + + // Change rows per page to 50 + await changeRowsPerPageAndValidate(paginationLocator, tableRowsLocator, 50); + + // Validate pagination controls + const leftArrow = page.locator('//div[contains(@class,"page-move-button")]').first(); + const rightArrow = page.locator('//div[contains(@class,"page-move-button")]').nth(1); + const pageRange = page.locator('//div[@class="page-display-part"]'); + + // Validate the displayed row range and total count + let rangeText = await pageRange.textContent(); + let [currentRange, totalCount] = rangeText!.split(" of "); + + if (parseInt(totalCount) > 50) { + + // Validate the initial range + await validateRange(pageRange, 1, 50); + + // Navigate to the next page using the right arrow and validate + await navigateAndValidate(rightArrow, tableRowsLocator, 50); + + // Validate the range after clicking right arrow + await validateRange(pageRange, 51, 100); + + // Navigate to the previous page using the left arrow and validate + await navigateAndValidate(leftArrow, tableRowsLocator, 50); + + // Validate the range after clicking left arrow + await validateRange(pageRange, 1, 50); + } + } else { + // If the pagination view not present in the page + expect(isPaginationVisible).toBe(false); + } + }); + +}); \ No newline at end of file diff --git a/ui-tests/tests/check_launcher_screen.spec.ts b/ui-tests/tests/check_launcher_screen.spec.ts new file mode 100644 index 00000000..d89e3f9f --- /dev/null +++ b/ui-tests/tests/check_launcher_screen.spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test, galata } from '@jupyterlab/galata'; + +test.describe('Launcher screen', () => { + test('Sanity: Can verify all the sections', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Fetch Dataproc plugin settings + const issues = await page.request.get('http://localhost:8888/dataproc-plugin/settings'); + expect(issues.ok()).toBeTruthy(); + + // Parse response and check BigQuery integration is enabled + const response = await issues.json(); + + // Check if the BigQuery integration is enabled + if (response.enable_bigquery_integration) { + await expect(page.getByRole('heading', { name: 'BigQuery Notebooks' })).toBeVisible(); + } else { + expect(response.enable_bigquery_integration).toBe(false); + } + + // Check visibility and validate Dataproc Serverless Notebooks section with new runtime template card is available + let sectionVisible = await page.getByRole('heading', { name: 'Dataproc Serverless Notebooks' }).isVisible(); + if (sectionVisible) { + await expect(page.getByRole('heading', { name: 'Dataproc Serverless Notebooks' })).toBeVisible(); + await expect(page.locator('.jp-LauncherCard:visible', { hasText: 'New Runtime Template' })).toBeVisible(); + } else { + throw new Error("Dataproc Serverless Notebooks section is missing"); + } + + // Check visibility and validate Dataproc Cluster Notebooks section + sectionVisible = await page.getByRole('heading', { name: 'Dataproc Cluster Notebooks' }).isVisible(); + if (sectionVisible) { + await expect(page.getByRole('heading', { name: 'Dataproc Cluster Notebooks' })).toBeVisible(); + await expect(page.locator('[data-category="Dataproc Cluster Notebooks"][title*="Apache Toree - Scala on"]').first()).toBeVisible(); + await expect(page.locator('[data-category="Dataproc Cluster Notebooks"][title*="R on"]').first()).toBeVisible(); + await expect(page.locator('[data-category="Dataproc Cluster Notebooks"][title*="PySpark on"]').first()).toBeVisible(); + await expect(page.locator('[data-category="Dataproc Cluster Notebooks"][title*="Python 3 on"]').first()).toBeVisible(); + } else { + throw new Error("Dataproc Cluster Notebooks section is missing"); + } + + // Verify Google Cloud Resources section along with cards is available + await expect(page.getByRole('heading', { name: 'Google Cloud Resources' })).toBeVisible(); + await expect(page.locator('[data-category="Google Cloud Resources"][title="Clusters"]')).toBeVisible(); + await expect(page.locator('[data-category="Google Cloud Resources"][title="Serverless"]')).toBeVisible(); + await expect(page.locator('[data-category="Google Cloud Resources"][title="Notebook Templates"]')).toBeVisible(); + await expect(page.locator('[data-category="Google Cloud Resources"][title="Scheduled Jobs"]')).toBeVisible(); + + }); +}); diff --git a/ui-tests/tests/check_notebook_templates.spec.ts b/ui-tests/tests/check_notebook_templates.spec.ts new file mode 100644 index 00000000..f91841c1 --- /dev/null +++ b/ui-tests/tests/check_notebook_templates.spec.ts @@ -0,0 +1,92 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test, galata } from '@jupyterlab/galata'; + +test.describe('Notebook Template tests', () => { + + // Set a common timeout for all tests + const timeout = 5 * 60 * 1000; + + // Function to navigate to Notebook Template page + async function navigateToNotebookTemplates(page) { + await page.locator('//*[@data-category="Google Cloud Resources" and @title="Notebook Templates"]').click(); + await page.getByText('Loading Templates').waitFor({ state: "detached" }); + await page.getByText('Loading Notebook Templates').waitFor({ state: "detached" }); + } + + test('Sanity: Can verify notebook templates page', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Notebook Templates page + await navigateToNotebookTemplates(page); + + // Check if notebook template table is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + // Check search field is present + await expect(page.getByPlaceholder('Filter Table')).toBeVisible(); + + // Check table headers + await expect(page.getByRole('columnheader', { name: 'Category' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Name' })).toBeVisible(); + await expect(page.getByRole('columnheader', { name: 'Description' })).toBeVisible(); + + // Check list of notebook tempaltes are displayed + const templatesCount = await page.locator('//table[@class="clusters-list-table"]//tr').count(); + expect(templatesCount).toBeGreaterThan(0); + + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); + + test('Can confirm the details by opening a notebook template', async ({ page }) => { + test.setTimeout(timeout); + + // Navigate to Notebook Templates page + await navigateToNotebookTemplates(page); + + // Check if notebook template table is present + const tableExists = await page.locator('//table[@class="clusters-list-table"]').isVisible(); + if (tableExists) { + // Get the nptebook template name + const templateName = await page.getByRole('cell').nth(1).textContent(); + + // Open 1st notebook template to use + await page.locator("//div[text()='Use this template']").first().click(); + + // Check if 'Select Kernel' dialog is appears on the screen and take the action + const isDialogPresent = await page.locator('//dialog[@aria-modal="true"]').isVisible(); + if (isDialogPresent) { + await page.getByRole('button', { name: 'Select Kernel' }).click(); + } + + // Check all the fields are present to use the template + await expect(page.getByRole('heading', { name: templateName! })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Create a duplicate of this cell below' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Move this cell up (Ctrl+Shift+up)' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Move this cell down (Ctrl+Shift+Down)' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Insert a cell above (A)' })).toBeVisible(); + await expect(page.getByLabel('Cells', { exact: true }).getByRole('button', { name: 'Insert a cell below (B)' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Delete this cell (D, D)' })).toBeVisible(); + + } else { + await expect(page.getByText('No rows to display')).toBeVisible(); + } + }); +}); \ No newline at end of file diff --git a/ui-tests/tests/check_serverless_batches_sessions.spec.ts b/ui-tests/tests/check_serverless_batches_sessions.spec.ts new file mode 100644 index 00000000..2db4c866 --- /dev/null +++ b/ui-tests/tests/check_serverless_batches_sessions.spec.ts @@ -0,0 +1,569 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test, galata } from '@jupyterlab/galata'; +import { Locator } from '@playwright/test'; + +test.describe('Serverless batches and sessions.', () => { + + // Helper function to navigate to the Serverless page and wait for it to load + async function navigateToServerless(page) { + await page.locator('//*[@data-category="Google Cloud Resources" and @title="Serverless"]').click(); + await page.getByText('Loading Batches').waitFor({ state: "detached" }); + } + + // Function to validate all the fields whihc are common for different Batch types + async function validateCommonFields(page) { + await expect(page.getByLabel('Custom container image')).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Specify a custom container image to add Java or Python dependencies not provided by the default container")]')).toBeVisible(); + await expect(page.getByLabel('Files', { exact: true })).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Files are included in the working directory of each executor. Can be a GCS file")]')).toBeVisible(); + await expect(page.getByLabel('Archive files')).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Archive files are extracted in the Spark working directory. Can be a GCS file")]')).toBeVisible(); + await expect(page.getByLabel('Arguments')).toBeVisible(); + await expect(page.getByText('Execution Configuration')).toBeVisible(); + await expect(page.getByLabel('Service account')).toBeVisible(); + await expect(page.getByText('Network Configuration')).toBeVisible(); + await expect(page.getByText('Establishes connectivity for the VM instances in this cluster.')).toBeVisible(); + + // Check Network Configuration 2 radio buttons + await expect(page.locator('//div[@class="create-runtime-radio"]/div[text()="Networks in this project"]')).toBeVisible(); + await expect(page.locator('//div[@class="create-batch-message" and contains(text(),"Networks shared from host project")]')).toBeVisible(); + + // Check by default 'Networks in this project' radio button is checked + await expect(page.locator('//div[@class="create-runtime-radio"]//input[@value="projectNetwork"]')).toBeChecked(); + + // Check Primary network and subnetwork fields having value 'default' + await expect(page.locator('//label[text()="Primary network*"]/following-sibling::div//input[@value="default"]')).toBeVisible(); + await expect(page.locator('//label[text()="subnetwork"]/following-sibling::div//input[@value="default"]')).toBeVisible(); + + await expect(page.getByLabel('Network tags')).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Network tags are text attributes you can add to make firewall rules")]')).toBeVisible(); + + // Check Encryption section + await expect(page.getByText('Encryption', { exact: true })).toBeVisible(); + await expect(page.getByText('Google-managed encryption key')).toBeVisible(); + await expect(page.getByText('No configuration required')).toBeVisible(); + await expect(page.getByText('Customer-managed encryption key (CMEK)')).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Google Cloud Key Management Service")]')).toBeVisible(); + + // Check by default 'Google-managed encryption key' radio button is checked + await expect(page.locator('//div[@class="create-batch-radio"]//input[@value="googleManaged"]').first()).toBeChecked(); + + // Check Peripheral Configuration section + await expect(page.getByText('Peripheral Configuration')).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Configure Dataproc to use Dataproc Metastore as its Hive metastore")]')).toBeVisible(); + await expect(page.locator('//*[contains(text(),"We recommend this option to persist table metadata when the batch finishes processing")]')).toBeVisible(); + + // Check History server cluster section + await expect(page.getByText('History server cluster').first()).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Choose a history server cluster to store logs in.")]')).toBeVisible(); + await expect(page.locator('//label[text()="History server cluster"]/following-sibling::div/input[@role="combobox"]')).toBeVisible(); + + // Check Properties section + await expect(page.getByText('Properties', { exact: true })).toBeVisible(); + await expect(page.getByRole('button', { name: 'ADD PROPERTY' })).toBeVisible(); + + // Check labels section + await expect(page.getByText('Labels')).toBeVisible(); + await expect(page.locator('//label[text()="Key 1*"]/following-sibling::div/input[@value="client"]')).toBeVisible(); + await expect(page.locator('//label[text()="Value 1"]/following-sibling::div/input[@value="dataproc-jupyter-plugin"]')).toBeVisible(); + await expect(page.getByRole('button', { name: 'ADD LABEL' })).toBeVisible(); + } + + test('Sanity: Can verify tabs and back button', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Verify the presence of Batches and Sessions tabs + await expect(page.getByText('Batches', { exact: true })).toBeVisible(); + await expect(page.getByText('Sessions', { exact: true })).toBeVisible(); + + // Verify the presence of the list table + await expect(page.locator('//table[@class="clusters-list-table"]')).toBeVisible(); + + // Click on Create Batch button and then Back button + await page.getByText('Create Batch').click(); + await page.locator('.back-arrow-icon > .icon-white > svg > path').click(); + }); + + test('Sanity: Can verify batches tab table headers', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Verify the presence of Batches and Sessions tabs + await expect(page.getByText('Batches', { exact: true })).toBeVisible(); + await expect(page.getByText('Sessions', { exact: true })).toBeVisible(); + + // Verify the presence of search field and column headers on Batches tab + await expect(page.getByPlaceholder('Filter Table')).toBeVisible(); + const headers = ['Batch ID', 'Status', 'Location', 'Creation time', 'Elapsed time', 'Type', 'Actions']; + for (const header of headers) { + await expect(page.getByRole('columnheader', { name: header, exact: true })).toBeVisible(); + } + }); + + test('Can click on Batch ID and validate the batch details', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Capture and click the first Batch ID + const batchId = await page.getByRole('cell').first().innerText(); + await page.getByRole('cell').first().click(); + + // Wait till Batch Details page loads + await page.getByText('Loading Batch Details').waitFor({ state: "detached" }); + + // Verify the batch details + await expect(page.locator('.back-arrow-icon > .icon-white > svg > path')).toBeVisible(); + await expect(page.locator(`//*[@class="cluster-details-title" and text()="${batchId}"]`)).toBeVisible(); + const buttons = ['CLONE', 'DELETE', 'VIEW SPARK LOGS', 'VIEW CLOUD LOGS']; + for (const button of buttons) { + await expect(page.getByRole('button', { name: button, exact: true })).toBeVisible(); + } + const texts = ['Batch ID', 'Batch UUID', 'Resource type', 'Status', 'Details', 'Properties', 'Environment config']; + for (const text of texts) { + await expect(page.getByText(text, { exact: true })).toBeVisible(); + } + }); + + test('Can validate all the fields and create a serverless batch with PySpark type', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page and click Create Batch button + await navigateToServerless(page); + await page.getByText('Create Batch').click(); + + // Capture the generated Batch ID for validation + const batchId = await page.getByLabel('Batch ID*').inputValue(); + + // Select PySpark batch type + await page.getByLabel('Open', { exact: true }).first().click(); + await page.getByRole('option', { name: 'PySpark' }).click(); + + // Validate all expected fields with default values are displayed + await expect(page.getByLabel('Runtime version*')).toBeVisible(); + await expect(page.getByLabel('Main python file*')).toBeVisible(); + await expect(page.locator('//*[@class="submit-job-message-input" and contains(text(),"Can be a GCS file with the gs:// prefix, an HDFS file on the")]')).toBeVisible(); + await expect(page.getByLabel('Main python file*')).toBeVisible(); + await expect(page.getByLabel('Additional python files')).toBeVisible(); + await expect(page.getByLabel('Jar files')).toBeVisible(); + + // Check all the displayed fields on the page + await validateCommonFields(page); + + // Enter all mandatory fields data to create a serverless batch + await page.getByLabel('Main python file*').fill('gs://us-central1-test-am-5a4039ed-bucket/dataproc-notebooks/wrapper_papermill.py'); + + // Select the project + await page.getByRole('combobox', { name: 'Project ID' }).click(); + await page.getByRole('combobox', { name: 'Project ID' }).fill('kokoro'); + await page.getByRole('option', { name: 'dataproc-kokoro-tests' }).click(); + await page.waitForTimeout(5000); + + // Select Metastore service + await page.getByRole('combobox', { name: 'Metastore services' }).click(); + await page.getByRole('option').first().click(); + + // Add Label + await page.getByRole('button', { name: 'ADD LABEL' }).click(); + await page.getByLabel('Key 2*').fill('goog-dataproc-location'); + await page.getByLabel('Value 2').fill('us-central1'); + + // Submit the batch and verify confirmation message + await page.waitForTimeout(2000); + await page.getByLabel('submit Batch').click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Batch ${batchId} successfully submitted`)).toBeVisible(); + }); + + test('Can validate all the fields and create a serverless batch with type Spark', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page and click Create Batch button + await navigateToServerless(page); + await page.getByText('Create Batch').click(); + + // Capture the generated Batch ID for validation + const batchId = await page.getByLabel('Batch ID*').inputValue(); + + // Check Batch type is Spark by default + const batchType = await page.getByLabel('Batch type*').inputValue(); + expect(batchType).toBe('Spark'); + + // Validate all expected fields with default values are displayed + await expect(page.getByLabel('Runtime version*')).toBeVisible(); + + await expect(page.getByText('Main class', { exact: true })).toBeVisible(); + await expect(page.locator('//*[contains(text(),"The fully qualified name of a class in a provided or standard jar file, for example, com.example.wordcount.")]')).toBeVisible(); + await expect(page.locator('//div[@class="create-batch-radio"]//input[@value="mainClass"]')).toBeChecked; + await expect(page.getByLabel('Main class*')).toBeVisible(); + await expect(page.getByText('Main jar URI')).toBeVisible(); + await expect(page.getByLabel('Jar files')).toBeVisible(); + + // Check all the displayed fields on the page + await validateCommonFields(page); + + // Enter all mandatory fields data to create a serverless batch + await page.getByLabel('Main class*').fill('org.apache.spark.examples.SparkPi'); + + // Select the project + await page.getByRole('combobox', { name: 'Project ID' }).click(); + await page.getByRole('combobox', { name: 'Project ID' }).fill('kokoro'); + await page.getByRole('option', { name: 'dataproc-kokoro-tests' }).click(); + await page.waitForTimeout(5000); + + // Select Metastore service + await page.getByRole('combobox', { name: 'Metastore services' }).click(); + await page.getByRole('option').first().click(); + + // Add Label + await page.getByRole('button', { name: 'ADD LABEL' }).click(); + await page.getByLabel('Key 2*').fill('goog-dataproc-location'); + await page.getByLabel('Value 2').fill('us-central1'); + + // Submit the batch and verify confirmation message + await page.waitForTimeout(2000); + await page.getByLabel('submit Batch').click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Batch ${batchId} successfully submitted`)).toBeVisible(); + }); + + test('Can validate all the fields and create a serverless batch with type SparkR', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page and click Create Batch button + await navigateToServerless(page); + await page.getByText('Create Batch').click(); + + // Capture the generated Batch ID for validation + const batchId = await page.getByLabel('Batch ID*').inputValue(); + + // Select PySpark batch type + await page.getByLabel('Open', { exact: true }).first().click(); + await page.getByRole('option', { name: 'SparkR' }).click(); + + // Validate all expected fields with default values are displayed + await expect(page.getByLabel('Runtime version*')).toBeVisible(); + await expect(page.getByLabel('Main R file*')).toBeVisible(); + await expect(page.locator('//*[@class="submit-job-message-input" and contains(text(),"Can be a GCS file with the gs://")]')).toBeVisible(); + + // Check all the displayed fields on the page + await validateCommonFields(page); + + // Enter all mandatory fields data to create a serverless batch + await page.getByLabel('Main R file*').fill('gs://dataproc-extension/DO NOT DELETE/helloworld.r'); + + // Select the project + await page.getByRole('combobox', { name: 'Project ID' }).click(); + await page.getByRole('combobox', { name: 'Project ID' }).fill('kokoro'); + await page.getByRole('option', { name: 'dataproc-kokoro-tests' }).click(); + await page.waitForTimeout(5000); + + // Select Metastore service + await page.getByRole('combobox', { name: 'Metastore services' }).click(); + await page.getByRole('option').first().click(); + + // Add Label + await page.getByRole('button', { name: 'ADD LABEL' }).click(); + await page.getByLabel('Key 2*').fill('goog-dataproc-location'); + await page.getByLabel('Value 2').fill('us-central1'); + + // Submit the batch and verify confirmation message + await page.waitForTimeout(2000); + await page.getByLabel('submit Batch').click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Batch ${batchId} successfully submitted`)).toBeVisible(); + }); + + test('Can validate all the fields and create a serverless batch with type SparkSql', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page and click Create Batch button + await navigateToServerless(page); + await page.getByText('Create Batch').click(); + + // Capture the generated Batch ID for validation + const batchId = await page.getByLabel('Batch ID*').inputValue(); + + // Select PySpark batch type + await page.getByLabel('Open', { exact: true }).first().click(); + await page.getByRole('option', { name: 'SparkSql' }).click(); + + // Validate all expected fields with default values are displayed + await expect(page.getByLabel('Query file*')).toBeVisible(); + await expect(page.locator('//*[@class="create-messagelist" and contains(text(),"Can be a GCS file with the gs://")]').first()).toBeVisible(); + + // Check all the displayed fields on the page + await expect(page.getByLabel('Custom container image')).toBeVisible(); + await expect(page.locator('//*[contains(text(),"Specify a custom container image to add Java or Python dependencies not provided by the default container")]')).toBeVisible(); + await expect(page.getByLabel('Jar files')).toBeVisible(); + await expect(page.locator('//*[@class="create-messagelist" and contains(text(),"Jar files are included in the CLASSPATH. Can be a GCS")]')).toBeVisible(); + + // Enter all mandatory fields data to create a serverless batch + await page.getByLabel('Query file*').fill('gs://dataproc-extension/DO NOT DELETE/sampleSQL.sql'); + + // Select the project + await page.getByRole('combobox', { name: 'Project ID' }).click(); + await page.getByRole('combobox', { name: 'Project ID' }).fill('kokoro'); + await page.getByRole('option', { name: 'dataproc-kokoro-tests' }).click(); + await page.waitForTimeout(5000); + + // Select Metastore service + await page.getByRole('combobox', { name: 'Metastore services' }).click(); + await page.getByRole('option').first().click(); + + // Add Label + await page.getByRole('button', { name: 'ADD LABEL' }).click(); + await page.getByLabel('Key 2*').fill('goog-dataproc-location'); + await page.getByLabel('Value 2').fill('us-central1'); + + // Submit the batch and verify confirmation message + await page.waitForTimeout(2000); + await page.getByLabel('submit Batch').click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Batch ${batchId} successfully submitted`)).toBeVisible(); + }); + + test('Can delete a batch using actions delete icon', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Get the first batch ID and delete it + const batchId = await page.getByRole('cell').first().innerText(); + await page.getByLabel('Delete Job').first().click(); + + // Confirm deletion and verify success message + await expect(page.getByText(`This will delete ${batchId} and cannot be undone.`)).toBeVisible(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Batch ${batchId} deleted successfully`)).toBeVisible(); + }); + + test('Can delete a batch from batch details page', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Capture and click the first Batch ID + const batchId = await page.getByRole('cell').first().innerText(); + await page.getByRole('cell').first().click(); + + // Click the delete button, confirm deletion, and verify success message + await page.getByRole('button', { name: 'DELETE', exact: true }).click(); + await expect(page.getByText(`This will delete ${batchId} and cannot be undone.`)).toBeVisible(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Batch ${batchId} deleted successfully`)).toBeVisible(); + }); + + test('Can clone the batch and create a new one', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Click on first Batch ID + await page.getByRole('cell').first().click(); + + // Wait till the Batch Details page loads + await page.getByText('Loading Batch Details').waitFor({ state: "detached" }); + + // Click on clone button, select network, and submit new batch + await page.getByRole('button', { name: 'CLONE' }).click(); + const batchId = await page.getByLabel('Batch ID*').inputValue(); + await page.getByRole('combobox', { name: 'Primary network*' }).click(); + await page.getByRole('option', { name: 'default' }).click(); + await page.getByLabel('Loading Spinner').waitFor({ state: 'detached' }); + await page.getByLabel('submit Batch').click(); + await page.waitForTimeout(5000); + await expect(page.getByText(`Batch ${batchId} successfully submitted`)).toBeVisible(); + }); + + test('Sanity: Can verify sessions tab table headers', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Click on Sessions tab and wait for it to load + await page.getByText('Sessions', { exact: true }).click(); + await page.getByText('Loading Sessions').waitFor({ state: "detached" }); + + // Verify search field and column headers on Sessions tab + await expect(page.getByPlaceholder('Filter Table')).toBeVisible(); + const headers = ['Session ID', 'Status', 'Location', 'Creator', 'Creation time', 'Elapsed time', 'Actions']; + for (const header of headers) { + await expect(page.getByRole('columnheader', { name: header, exact: true })).toBeVisible(); + } + }); + + test('Can click on Session ID and validate the session details', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page and switch to Sessions tab + await navigateToServerless(page); + await page.getByText('Sessions', { exact: true }).click(); + await page.getByText('Loading Sessions').waitFor({ state: "detached" }); + + // Wait for Sessions to load + await page.getByText('Loading Sessions').waitFor({ state: "detached" }); + + // Click on first session id + await page.getByRole('cell').first().click(); + + // Wait for Session Details to load + await page.getByText('Loading Session Details').waitFor({ state: "detached" }); + + // Verify the session details + await expect(page.locator('.back-arrow-icon > .icon-white > svg > path')).toBeVisible(); + await expect(page.getByLabel('Serverless').getByText('Name', { exact: true })).toBeVisible(); + const expectedTexts = [ + 'Session details', 'UUID', 'Status', 'Create time', 'Details' + ]; + for (const text of expectedTexts) { + await expect(page.getByText(text, { exact: true })).toBeVisible(); + } + const expectedButtons = ['TERMINATE', 'VIEW SPARK LOGS', 'VIEW CLOUD LOGS']; + for (const button of expectedButtons) { + await expect(page.getByRole('button', { name: button, exact: true })).toBeVisible(); + } + }); + + test('Can delete a session', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page and switch to Sessions tab + await navigateToServerless(page); + await page.getByText('Sessions', { exact: true }).click(); + + // Wait till the page loads + await page.getByText('Loading Sessions').waitFor({ state: "detached" }); + + // Get the first session id + const sessionId = await page.getByRole('cell').first().innerText(); + + // Delete the first session + await page.getByTitle('Delete Session').first().click(); + + // Verify the text on confirmation popup and click on delete option + await expect(page.getByText('This will delete ' + sessionId + ' and cannot be undone.')).toBeVisible(); + await page.getByRole('button', { name: 'Delete' }).click(); + await page.waitForTimeout(5000); + await expect(page.getByText('Session ' + sessionId + ' deleted successfully')).toBeVisible(); + }); + + test('Check pagination in serverless batches tab', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Navigate to Serverless page + await navigateToServerless(page); + + // Check if pagination is visible on the page + const paginationLocator = page.locator('//div[@class="pagination-parent-view"]'); + const isPaginationVisible = await paginationLocator.isVisible(); + + // Function to validate row count + const validateRowCount = async (tableRowsLocator: Locator, expectedCount: number) => { + const rowCount = await tableRowsLocator.count(); + expect(rowCount).toBeLessThanOrEqual(expectedCount); + }; + + // Function to change rows per page and validate + const changeRowsPerPageAndValidate = async (paginationLocator: Locator, tableRowsLocator: Locator, newRowsPerPage: number) => { + let currentRowsPerPage = await paginationLocator.locator('input').getAttribute('value'); + await page.getByText(currentRowsPerPage!, { exact: true }).click(); + await page.getByRole('option', { name: String(newRowsPerPage) }).click(); + await page.waitForTimeout(2000); + await validateRowCount(tableRowsLocator, newRowsPerPage); + }; + + // Function to navigate and validate row count + const navigateAndValidate = async (buttonLocator: Locator, tableRowsLocator: Locator, expectedCount: number) => { + await buttonLocator.click(); + await page.waitForTimeout(2000); + await validateRowCount(tableRowsLocator, expectedCount); + }; + + // Function to validate the range + const validateRange = async (pageRange: Locator, expectedStart: number, expectedEnd: number) => { + const rangeText = await pageRange.textContent(); + const [currentRange, totalCount] = rangeText!.split(" of "); + const [start, end] = currentRange.split(" - ").map(Number); + expect(start).toBe(expectedStart); + expect(end).toBeLessThanOrEqual(expectedEnd); + }; + + if (isPaginationVisible) { + const tableRowsLocator = page.locator('//tbody[@class="clusters-table-body"]/tr[@role="row"]'); + + // Verify the default row count and selected pagination number + await validateRowCount(tableRowsLocator, 50); + let currentRowsPerPage = await paginationLocator.locator('input').getAttribute('value'); + expect(currentRowsPerPage).toBe('50'); + + // Change rows per page to 100 and validate + await changeRowsPerPageAndValidate(paginationLocator, tableRowsLocator, 100); + + // Change rows per page to 200 and validate + await changeRowsPerPageAndValidate(paginationLocator, tableRowsLocator, 200); + + // Change back rows per page to 50 + await changeRowsPerPageAndValidate(paginationLocator, tableRowsLocator, 50); + + // Validate pagination controls + const leftArrow = page.locator('//div[contains(@class,"page-move-button")]').first(); + const rightArrow = page.locator('//div[contains(@class,"page-move-button")]').nth(1); + const pageRange = page.locator('//div[@class="page-display-part"]'); + + // Validate the displayed row range and total count + let rangeText = await pageRange.textContent(); + let [currentRange, totalCount] = rangeText!.split(" of "); + + if (parseInt(totalCount) > 50) { + + // Validate the initial range + await validateRange(pageRange, 1, 50); + + // Navigate to the next page using the right arrow and validate + await navigateAndValidate(rightArrow, tableRowsLocator, 50); + + // Validate the range after clicking right arrow + await validateRange(pageRange, 51, 100); + + // Navigate to the previous page using the left arrow and validate + await navigateAndValidate(leftArrow, tableRowsLocator, 50); + + // Validate the range after clicking left arrow + await validateRange(pageRange, 1, 50); + } + } else { + // If the pagination view not present in the page + expect(isPaginationVisible).toBe(false); + } + }); +}); + diff --git a/ui-tests/tests/create_runtimeTemplate_configScreen.spec.ts b/ui-tests/tests/create_runtimeTemplate_configScreen.spec.ts new file mode 100644 index 00000000..bffa3a03 --- /dev/null +++ b/ui-tests/tests/create_runtimeTemplate_configScreen.spec.ts @@ -0,0 +1,107 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test, galata } from '@jupyterlab/galata'; + +test.describe('Create serverless notebook from config screen', () => { + + // Generate formatted current date string + const now = new Date(); + const pad = (num: number) => String(num).padStart(2, '0'); + const dateTimeStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(Math.floor(now.getMinutes() / 5) * 5)}:${pad(now.getSeconds())}`; + + // Create template name + const templateName = 'auto-test-' + dateTimeStr; + + test('Can create serverless notebook', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Goto menu and click on Google setting + await page + .getByLabel('main menu', { exact: true }) + .getByText('Settings') + .click(); + const dataprocSettings = page.getByText('Google Dataproc Settings'); + const bigQuerySettings = page.getByText('Google BigQuery Settings'); + await dataprocSettings.or(bigQuerySettings).click(); + + await page.getByText('Loading Config Setup').waitFor({ state: "detached" }); + + // Click on Create button + await page.getByText('Create', { exact: true }).click(); + await page.getByText('Loading Runtime').waitFor({ state: "detached" }); + + // Enter all the details to create Severless runtime template + await page.getByLabel('Display name*').click(); + await page.getByLabel('Display name*').fill(templateName); + await page.getByLabel('Description*').click(); + await page.getByLabel('Description*').fill('Testing'); + + // Select the project + await page.getByRole('combobox', { name: 'Project ID' }).click(); + await page.getByRole('combobox', { name: 'Project ID' }).fill('dataproc-jupyter-extension-dev'); + await page.getByRole('option', { name: 'dataproc-jupyter-extension-dev' }).click(); + await page.waitForTimeout(5000); + + // Select Metastore service + await page.getByRole('combobox', { name: 'Metastore services' }).click(); + const firstOption = await page.getByRole('option').first(); + await firstOption.click(); + await page.waitForTimeout(2000); + + // Click on save button to create a notebook + await page.getByText('SAVE', { exact: true }).click(); + await page.waitForTimeout(5000); + + // Check the notebook created confirmation message + await expect(page.getByText('Runtime Template ' + templateName + ' successfully created')).toBeVisible(); + }); + + test('Can edit created serverless notebook', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Goto menu and click on Google setting + await page + .getByLabel('main menu', { exact: true }) + .getByText('Settings') + .click(); + const dataprocSettings = page.getByText('Google Dataproc Settings'); + const bigQuerySettings = page.getByText('Google BigQuery Settings'); + await dataprocSettings.or(bigQuerySettings).click(); + + // Wait till the page loaded + await page.getByText('Loading Config Setup').waitFor({ state: "detached" }); + await page.getByText('Loading Runtime Templates').waitFor({ state: "detached" }); + + await page.getByPlaceholder('Filter Table').first().fill(templateName); + + await page.getByRole('cell', { name: templateName }).click(); + await page.getByText('Loading Runtime').waitFor({ state: "detached" }); + + await page.getByLabel('Description*').clear(); + await page.getByLabel('Description*').fill('testing for edit runtime template'); + await page.getByRole('progressbar').waitFor({ state: "detached" }); + await page.waitForTimeout(3000); + + // Click on save button to update a notebook + await page.getByText('SAVE', { exact: true }).click(); + await page.waitForTimeout(5000); + + // Check the notebook updated confirmation message + await expect(page.getByText('Runtime Template ' + templateName + ' successfully updated')).toBeVisible(); + }); +}); diff --git a/ui-tests/tests/create_runtimeTemplate_launcher.spec.ts b/ui-tests/tests/create_runtimeTemplate_launcher.spec.ts new file mode 100644 index 00000000..9d15eea4 --- /dev/null +++ b/ui-tests/tests/create_runtimeTemplate_launcher.spec.ts @@ -0,0 +1,323 @@ +/** + * @license + * Copyright 2024 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { expect, test, galata } from '@jupyterlab/galata'; + +test.describe('Serverless notebook from launcher screen', () => { + + test('Sanity: Can perform field validation', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Click on Severless New Runtime Template + await page + .locator('.jp-LauncherCard:visible', { + hasText: 'New Runtime Template' + }) + .click(); + await page.getByText('Loading Runtime').waitFor({ state: "detached" }); + + await page.getByLabel('Display name*').click(); + await page.getByLabel('Display name*').fill('testing246'); + await page.getByLabel('Display name*').clear(); + await expect(page.getByText('Name is required')).toBeVisible(); // Error should appear for empty Display name + + // Fill Display name and check if error is hidden + await page.getByLabel('Display name*').fill('testing246'); + await expect(page.getByText('Name is required')).toBeHidden(); + + await page.getByLabel('Runtime ID*').clear(); + await expect(page.getByText('ID is required')).toBeVisible(); // Error should appear for empty Runtime ID + + // Fill Runtime ID and check if error is hidden + await page.getByLabel('Runtime ID*').fill('runtime123'); + await expect(page.getByText('ID is required')).toBeHidden(); + + await page.getByLabel('Description*').fill('test description'); + await page.getByLabel('Description*').clear(); + await expect(page.getByText('Description is required')).toBeVisible(); // Error should appear for empty Description + + // Fill Description and check if error is hidden + await page.getByLabel('Description*').fill('test description'); + await expect(page.getByText('Description is required')).toBeHidden(); + + await page.getByLabel('Runtime version*').clear(); + await expect(page.getByText('Version is required')).toBeVisible(); // Error should appear for empty Runtime version + + // Fill Runtime version and check if error is hidden + await page.getByLabel('Runtime version*').fill('2.2'); + await expect(page.getByText('Version is required')).toBeHidden(); + + // Add property validation: Empty key should show an error + await page.getByRole('button', { name: 'ADD PROPERTY' }).click(); + await expect(page.getByText('key is required')).toBeVisible(); // Key field should be required + + // Check if the ADD PROPERTY button is disabled + let isDisabled = await page.getByRole('button', { name: 'ADD PROPERTY' }).getAttribute('class'); + expect(isDisabled).toContain('disabled'); // Button should be disabled initially + + // Fill the key field and ensure the error is hidden + await page.getByLabel('Key 1*').first().fill('key'); + await expect(page.getByText('key is required')).toBeHidden(); // Error should disappear after entering the key + + // Click on the value field and ensure the ADD PROPERTY button is enabled + await page.getByLabel('Value').first().click(); + isDisabled = await page.getByRole('button', { name: 'ADD PROPERTY' }).getAttribute('class'); + expect(isDisabled).not.toContain('disabled'); // Button should now be enabled + + // Delete added property + await page.locator('.labels-delete-icon').click(); + + // Check labels section + await expect(page.getByText('Labels',{ exact: true })).toBeVisible(); + await expect(page.locator('//label[text()="Key 1*"]/following-sibling::div/input[@value="client"]')).toBeVisible(); + await expect(page.locator('//label[text()="Value 1"]/following-sibling::div/input[@value="dataproc-jupyter-plugin"]')).toBeVisible(); + await expect(page.getByRole('button', { name: 'ADD LABEL' })).toBeVisible(); + await page.getByRole('button', { name: 'ADD LABEL' }).click(); + await expect(page.getByText('key is required')).toBeVisible(); // Key field should be required + + // Fill the key field and ensure the error is hidden + await page.getByLabel('Key 2*').first().fill('key'); + await expect(page.getByText('key is required')).toBeHidden(); // Error should disappear after entering the key + await page.getByLabel('Value 2').first().click(); + + // Delete added label + await page.locator('.labels-delete-icon').click(); + + }); + + test('Sanity: Can create serverless notebook', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + // Click on Severless New Runtime Template + await page + .locator('.jp-LauncherCard:visible', { + hasText: 'New Runtime Template' + }) + .click(); + await page.getByText('Loading Runtime').waitFor({ state: "detached" }); + + // Generate formatted current date string + const now = new Date(); + const pad = (num: number) => String(num).padStart(2, '0'); + const dateTimeStr = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(Math.floor(now.getMinutes() / 5) * 5)}:${pad(now.getSeconds())}`; + + // Create template name + const templateName = 'auto-test-' + dateTimeStr; + + // Fill the template details + await page.getByLabel('Display name*').click(); + await page.getByLabel('Display name*').fill(templateName); + await page.getByLabel('Description*').click(); + await page.getByLabel('Description*').fill('Testing'); + + // Select the project + await page.getByRole('combobox', { name: 'Project ID' }).click(); + await page.getByRole('combobox', { name: 'Project ID' }).fill('kokoro'); + await page.getByRole('option', { name: 'dataproc-kokoro-tests' }).click(); + await page.waitForTimeout(5000); + + // Select Metastore service + await page.getByRole('combobox', { name: 'Metastore services' }).click(); + await page.waitForTimeout(2000); + const firstOption = await page.getByRole('option').first(); + await firstOption.click(); + await page.waitForTimeout(2000); + + // Click on save button to create a notebook + await page.getByText('SAVE', { exact: true }).click(); + await page.waitForTimeout(5000); + + // Check the notebook created confirmation message + await expect(page.getByText('Runtime Template ' + templateName + ' successfully created')).toBeVisible(); + await page.waitForTimeout(5000); + + // Check the created notebook on launcher screen + await expect(page + .locator('.jp-LauncherCard:visible', { + hasText: templateName +' on Serverless Spark (Remote)' + })).toBeVisible(); + }); + + // Navigate to config setup page and click on create template button + async function navigateToRuntimeTemplate(page) { + await page.getByLabel('main menu', { exact: true }).getByText('Settings').click(); + const dataprocSettings = page.getByText('Google Dataproc Settings'); + const bigQuerySettings = page.getByText('Google BigQuery Settings'); + await dataprocSettings.or(bigQuerySettings).click(); + await page.getByText('Create', { exact: true }).click(); + await page.getByText('Loading Runtime').waitFor({ state: "hidden" }); + } + + // Check if spark properties are visible + async function checkSparkProperties(page) { + const properties = [ + 'spark.driver.cores', 'spark.driver.memory', 'spark.driver.memoryOverhead', + 'spark.dataproc.driver.disk.size', 'spark.dataproc.driver.disk.tier', + 'spark.executor.cores', 'spark.executor.memory', 'spark.executor.memoryOverhead', + 'spark.dataproc.executor.disk.size', 'spark.dataproc.executor.disk.tier', + 'spark.executor.instances' + ]; + for (const prop of properties) { + await expect(page.locator(`//*[@value="${prop}"]`)).toBeVisible(); + } + } + + // Check if default values for properties match expected values + async function checkPropertiesValue(page, values) { + for (const [id, value] of Object.entries(values)) { + const actualValue = await page.locator(`//*[@id="value-${id}"]//input`).getAttribute('value'); + expect(actualValue).toBe(value); + } + } + + test('Sanity: Can check all spark properties are displayed', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + await navigateToRuntimeTemplate(page); + + // Verify sections and subsections presence + const sections = ['Spark Properties', 'Resource Allocation', 'Autoscaling', 'GPU']; + for (const section of sections) { + await expect(page.getByText(section, { exact: true })).toBeVisible(); + } + + // Expand and check properties in Resource Allocation subsection + await page.locator('//*[@id="resource-allocation-expand-icon"]').click(); + await page.mouse.wheel(0, 300); // Scroll down to reveal properties + await checkSparkProperties(page); + + // Verify default values in Resource Allocation subsection + const allocationValues = { + 'spark.driver.cores': '4', + 'spark.driver.memory': '12200m', + 'spark.driver.memoryOverhead': '1220m', + 'spark.dataproc.driver.disk.size': '400g', + 'spark.dataproc.driver.disk.tier': 'standard', + 'spark.executor.cores': '4', + 'spark.executor.memory': '12200m', + 'spark.executor.memoryOverhead': '1220m', + 'spark.dataproc.executor.disk.size': '400g', + 'spark.dataproc.executor.disk.tier': 'standard', + 'spark.executor.instances': '2' + }; + await checkPropertiesValue(page, allocationValues); + + // Expand and check properties in Resource Autoscaling subsection + await page.locator('//*[@id="autoscaling-expand-icon"]').click(); + await page.mouse.wheel(0, 300); // Scroll down to reveal properties + const autoscalingProps = [ + 'spark.dynamicAllocation.enabled', 'spark.dynamicAllocation.initialExecutors', + 'spark.dynamicAllocation.minExecutors', 'spark.dynamicAllocation.maxExecutors', + 'spark.dynamicAllocation.executorAllocationRatio', 'spark.reducer.fetchMigratedShuffle.enabled' + ]; + for (const prop of autoscalingProps) { + await expect(page.locator(`//*[@value="${prop}"]`)).toBeVisible(); + } + + // Verify default values in Resource Autoscaling subsection + const autoscalingValues = { + 'spark.dynamicAllocation.enabled': 'true', + 'spark.dynamicAllocation.initialExecutors': '2', + 'spark.dynamicAllocation.minExecutors': '2', + 'spark.dynamicAllocation.maxExecutors': '1000', + 'spark.dynamicAllocation.executorAllocationRatio': '0.3', + 'spark.reducer.fetchMigratedShuffle.enabled': 'false' + }; + await checkPropertiesValue(page, autoscalingValues); + + // Verify GPU subsection is unchecked by default + const isChecked = await page.getByLabel('GPU').isChecked(); + expect(isChecked).toBe(false); + + // Check GPU checkbox and validate properties are visible + await page.getByLabel('GPU').check(); + const gpuProps = [ + 'spark.dataproc.driverEnv.LANG', 'spark.executorEnv.LANG', 'spark.dataproc.executor.compute.tier', + 'spark.dataproc.executor.resource.accelerator.type', 'spark.plugins', 'spark.task.resource.gpu.amount', + 'spark.shuffle.manager' + ]; + for (const prop of gpuProps) { + await expect(page.locator(`//*[@value="${prop}"]`)).toBeVisible(); + } + + // Verify default values in GPU subsection + const gpuValues = { + 'spark.dataproc.driverEnv.LANG': 'C.UTF-8', + 'spark.executorEnv.LANG': 'C.UTF-8', + 'spark.dataproc.executor.compute.tier': 'premium', + 'spark.dataproc.executor.resource.accelerator.type': 'l4', + 'spark.plugins': 'com.nvidia.spark.SQLPlugin', + 'spark.task.resource.gpu.amount': '0.25', + 'spark.shuffle.manager': 'com.nvidia.spark.rapids.RapidsShuffleManager' + }; + await checkPropertiesValue(page, gpuValues); + }); + + test('Can check allocation subsection properties changes when GPU is selected and unselected', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + await navigateToRuntimeTemplate(page); + + // Expand Resource Allocation subsection + await page.locator('//*[@id="resource-allocation-expand-icon"]').click(); + + // Check GPU checkbox and validate the properties + await page.getByLabel('GPU').check(); + + let sDEDiskTierValue = { + 'spark.dataproc.executor.disk.tier': 'premium', + }; + await checkPropertiesValue(page, sDEDiskTierValue); + + const hiddenProps = ['spark.executor.memoryOverhead', 'spark.dataproc.executor.disk.size']; + for (const prop of hiddenProps) { + await expect(page.locator(`//*[@value="${prop}"]`)).toBeHidden(); + } + + // Uncheck GPU checkbox and validate the properties + await page.getByLabel('GPU').uncheck(); + + sDEDiskTierValue = { + 'spark.dataproc.executor.disk.tier': 'standard', + }; + await checkPropertiesValue(page, sDEDiskTierValue); + + const visibleProps = ['spark.executor.memoryOverhead', 'spark.dataproc.executor.disk.size']; + for (const prop of visibleProps) { + await expect(page.locator(`//*[@value="${prop}"]`)).toBeVisible(); + } + }); + + test('Can verify by changing to non-L4 value', async ({ page }) => { + test.setTimeout(5 * 60 * 1000); + + await navigateToRuntimeTemplate(page); + + // Expand Resource Allocation subsection + await page.locator('//*[@class="spark-properties-sub-header-parent"][1]/div[2]').click(); + + // Change GPU type to non-L4 value and validate properties are visible + await page.getByLabel('GPU').check(); + const sparkDPERATypeInput = page.locator('//*[@id="value-spark.dataproc.executor.resource.accelerator.type"]//input'); + await sparkDPERATypeInput.fill('a100-40'); + + // Check Allocation subsection property is visible + await expect(page.locator('//*[@value="spark.dataproc.executor.disk.size"]')).toBeVisible(); + + }); + +}); diff --git a/ui-tests/tests/notebook_scheduler.spec.ts b/ui-tests/tests/notebook_scheduler.spec.ts index 8f936e56..9ff61003 100644 --- a/ui-tests/tests/notebook_scheduler.spec.ts +++ b/ui-tests/tests/notebook_scheduler.spec.ts @@ -23,7 +23,7 @@ async function checkInputNotEmpty(page, label) { return value.trim() !== ''; } -test('Job Scheduler', async ({ page }) => { +test('Sanity: Job Scheduler', async ({ page }) => { test.setTimeout(150 * 1000); let clusterNotEmpty = true; await page.getByRole('region', { name: 'notebook content' }).click(); diff --git a/ui-tests/tests/settings_menu.spec.ts b/ui-tests/tests/settings_menu.spec.ts index 77db5e4a..2b4b4bac 100644 --- a/ui-tests/tests/settings_menu.spec.ts +++ b/ui-tests/tests/settings_menu.spec.ts @@ -18,7 +18,7 @@ import { test, expect, galata } from '@jupyterlab/galata'; test.describe('Settings Menu', () => { - test('Can find settings menu', async ({ page }) => { + test('Sanity: Can find settings menu', async ({ page }) => { await page .getByLabel('main menu', { exact: true }) .getByText('Settings') @@ -28,7 +28,7 @@ test.describe('Settings Menu', () => { await dataprocSettings.or(bigQuerySettings).click(); }); - test('Can change project', async ({ page }) => { + test('Sanity: Can change project', async ({ page }) => { await page .getByLabel('main menu', { exact: true }) .getByText('Settings')