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')