This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/zeppelin.git


The following commit(s) were added to refs/heads/master by this push:
     new 3ce7b63e35 [ZEPPELIN-6362] Add E2E coverage for interpreter 
configuration
3ce7b63e35 is described below

commit 3ce7b63e3593aceb21dc0aa9b1a886c553d99dbf
Author: Minho Jang <[email protected]>
AuthorDate: Sat Nov 14 18:56:41 2026 +0900

    [ZEPPELIN-6362] Add E2E coverage for interpreter configuration
    
    ### What is this PR for?
    Add Playwright E2E coverage for the existing Angular interpreter route 
before future React migration work.
    
    Cover:
    - Direct navigation, reload, settings display and case-insensitive search.
    - Interpreter creation, required fields, duplicate names and group defaults.
    - Typed properties, dependency editing and cancellation.
    - Binding options, impersonation and conditional process and permission 
fields.
    - Restart and removal confirmations, including cancellation.
    - Repository display, protected defaults, tooltips and add/remove workflows.
    - Repository form validation, URL protocols, credentials and proxy settings.
    
    Tests use real server reads by default. Endpoint-specific stubs provide 
controlled states that are not guaranteed in the default environment. Mutation 
requests are intercepted to avoid persisting configuration changes or 
restarting interpreters.
    
    ### What type of PR is it?
    Improvement
    
    ### What is the Jira issue?
    * https://issues.apache.org/jira/browse/ZEPPELIN-6362
    
    ### How should this be tested?
    With the Zeppelin server running at `http://localhost:8080`, run from 
`zeppelin-web-angular`:
    
    ```bash
    npm run e2e:fast -- tests/workspace/interpreter --retries=0
    ```
    
    ### Questions:
    - Does the license files need to update? No.
    - Is there breaking changes for older versions? No.
    - Does this needs documentation? No.
    
    Closes #5612 from miinhho/ZEPPELIN-6362-interpreter-e2e.
    
    Signed-off-by: YONGJAE LEE <[email protected]>
---
 .../e2e/models/interpreter-page.ts                 | 145 ++++++
 .../e2e/models/interpreter-repository-modal.ts     |  48 +-
 .../e2e/models/interpreter-repository-page.ts      |  51 ++
 .../e2e/models/interpreter-test.util.ts            | 117 +++++
 .../create-repository-proxy-credentials.spec.ts    |  32 +-
 .../interpreter/interpreter-page-structure.spec.ts |  82 +++
 .../interpreter/interpreter-repositories.spec.ts   | 397 +++++++++++++++
 .../interpreter/interpreter-settings.spec.ts       | 563 +++++++++++++++++++++
 .../create-repository-modal.component.html         |   7 +-
 .../interpreter/interpreter.component.html         |   7 +-
 .../workspace/interpreter/item/item.component.html |  19 +-
 11 files changed, 1424 insertions(+), 44 deletions(-)

diff --git a/zeppelin-web-angular/e2e/models/interpreter-page.ts 
b/zeppelin-web-angular/e2e/models/interpreter-page.ts
new file mode 100644
index 0000000000..47836486cf
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/interpreter-page.ts
@@ -0,0 +1,145 @@
+/*
+ * 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 { Locator, Page } from '@playwright/test';
+
+export class InterpreterPage {
+  readonly title: Locator;
+  readonly description: Locator;
+  readonly search: Locator;
+  readonly repositoryButton: Locator;
+  readonly createCard: Locator;
+  readonly settings: Locator;
+  readonly settingTitles: Locator;
+
+  constructor(readonly page: Page) {
+    this.title = page.getByRole('heading', { level: 2, name: 
/^Interpreters(?:\s|$)/ });
+    this.description = page.getByText('Manage interpreters settings.', { 
exact: false });
+    this.search = page.getByPlaceholder('Search interpreters...', { exact: 
true });
+    this.repositoryButton = page.getByRole('button', { name: 'Repository', 
exact: true });
+    this.createCard = page.getByText('Create', { exact: true });
+    this.settings = page.getByTestId('interpreter-setting');
+    this.settingTitles = this.settings
+      .getByRole('heading', { level: 3 })
+      .filter({ has: page.getByTestId('interpreter-status') });
+  }
+
+  setting(name: string): InterpreterItemPage {
+    const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
+    return new InterpreterItemPage(
+      this.page,
+      this.settings.filter({
+        has: this.page.getByRole('heading', { level: 3, name: new 
RegExp(`^${escaped}\\s+%`) })
+      })
+    );
+  }
+
+  createForm(): InterpreterItemPage {
+    return new InterpreterItemPage(this.page, 
this.page.getByTestId('interpreter-create-form'));
+  }
+}
+
+export class InterpreterItemPage {
+  readonly edit: Locator;
+  readonly restart: Locator;
+  readonly remove: Locator;
+  readonly save: Locator;
+  readonly cancel: Locator;
+  readonly name: Locator;
+  readonly group: Locator;
+  readonly status: Locator;
+  readonly properties: Locator;
+  readonly dependencies: Locator;
+  readonly newProperty: Locator;
+  readonly propertyKey: Locator;
+  readonly propertyValue: Locator;
+  readonly propertyType: Locator;
+  readonly artifact: Locator;
+  readonly exclusions: Locator;
+  readonly existingProcess: Locator;
+  readonly host: Locator;
+  readonly port: Locator;
+  readonly setPermission: Locator;
+  readonly owners: Locator;
+  readonly tooltip: Locator;
+  readonly bindingMenu: Locator;
+  readonly bindingMode: Locator;
+
+  constructor(
+    readonly page: Page,
+    readonly card: Locator
+  ) {
+    this.edit = card.getByRole('button', { name: 'Edit', exact: true });
+    this.restart = card.getByRole('button', { name: 'Restart', exact: true });
+    this.remove = card.getByRole('button', { name: 'Remove', exact: true });
+    this.save = card.getByRole('button', { name: 'Save', exact: true });
+    this.cancel = card.getByRole('button', { name: 'Cancel', exact: true });
+    this.name = card.getByPlaceholder('interpreter name', { exact: true });
+    this.group = card.getByTestId('interpreter-group');
+    this.status = card.getByTestId('interpreter-status');
+    this.properties = card.getByTestId('interpreter-properties');
+    this.dependencies = card.getByTestId('interpreter-dependencies');
+    this.newProperty = this.properties
+      .getByRole('row')
+      .filter({ has: page.getByRole('button', { name: 'Add property', exact: 
true }) });
+    this.propertyKey = 
this.newProperty.getByRole('cell').nth(0).getByRole('textbox');
+    this.propertyValue = 
this.newProperty.getByRole('cell').nth(1).locator('input:not([readonly]), 
textarea');
+    this.propertyType = this.newProperty.locator('.type-selector');
+    this.artifact = card.getByPlaceholder('groupId:artifactId:version or local 
file path', { exact: true });
+    this.exclusions = card.getByPlaceholder('(Optional) comma separated 
groupId:artifactId list', { exact: true });
+    this.existingProcess = card.getByRole('checkbox', { name: 'Connect to 
existing process', exact: true });
+    this.host = card.getByRole('textbox', { name: 'Host', exact: true });
+    this.port = card.getByRole('spinbutton', { name: 'Port', exact: true });
+    this.setPermission = card.getByRole('checkbox', { name: 'Set permission', 
exact: true });
+    this.owners = card.getByTestId('interpreter-owners');
+    this.tooltip = page.locator('.ant-tooltip-inner:visible');
+    this.bindingMenu = page.locator('.ant-dropdown:visible');
+    this.bindingMode = card.locator('.ant-dropdown-trigger').first();
+  }
+
+  propertyRow(key: string): Locator {
+    return this.properties.getByRole('row').filter({ has: 
this.page.getByRole('cell', { name: key, exact: true }) });
+  }
+
+  propertyCell(key: string): Locator {
+    return this.propertyRow(key).getByRole('cell').nth(1);
+  }
+
+  passwordValue(key: string): Locator {
+    return this.propertyCell(key).locator('input[type="password"]');
+  }
+
+  editableDependencyRow(index: number): Locator {
+    return this.dependencies
+      .getByRole('row')
+      .filter({
+        has: this.page.getByRole('button', { name: 'Remove dependency', exact: 
true })
+      })
+      .nth(index);
+  }
+
+  dependencyRow(artifact: string): Locator {
+    return this.dependencies
+      .getByRole('row')
+      .filter({ has: this.page.getByRole('cell', { name: artifact, exact: true 
}) });
+  }
+
+  async selectGroup(name: string): Promise<void> {
+    await this.group.click();
+    await this.page.locator('.ant-select-dropdown:visible').getByText(name, { 
exact: true }).click();
+  }
+
+  async selectPropertyType(name: string): Promise<void> {
+    await this.propertyType.click();
+    await this.page.locator('.ant-select-dropdown:visible').getByText(name, { 
exact: true }).click();
+  }
+}
diff --git a/zeppelin-web-angular/e2e/models/interpreter-repository-modal.ts 
b/zeppelin-web-angular/e2e/models/interpreter-repository-modal.ts
index c4fc0d1782..9c635fd628 100644
--- a/zeppelin-web-angular/e2e/models/interpreter-repository-modal.ts
+++ b/zeppelin-web-angular/e2e/models/interpreter-repository-modal.ts
@@ -23,23 +23,44 @@ export interface RepositoryProxyCredentials {
 export class InterpreterRepositoryModal extends BasePage {
   readonly repositoryTrigger: Locator;
   readonly createRepositoryTag: Locator;
+  readonly dialog: Locator;
   readonly idInput: Locator;
   readonly urlInput: Locator;
+  readonly urlProtocol: Locator;
+  readonly snapshotSwitch: Locator;
+  readonly usernameInput: Locator;
+  readonly passwordInput: Locator;
+  readonly proxyHttpRadio: Locator;
+  readonly proxyHttpsRadio: Locator;
+  readonly proxyHostInput: Locator;
+  readonly proxyPortInput: Locator;
   readonly proxyLoginInput: Locator;
   readonly proxyPasswordInput: Locator;
   readonly addButton: Locator;
+  readonly dismissButton: Locator;
 
   constructor(page: Page) {
     super(page);
-    this.repositoryTrigger = page.locator('button.repository-trigger');
-    this.createRepositoryTag = page.locator('nz-tag.editable-tag');
-    this.idInput = page.locator('input[placeholder="Repository id"]');
-    this.urlInput = page.locator('input[placeholder="Repository url"]');
-    // Locate the proxy inputs by placeholder, not formControlName: the 
binding is
-    // what the regression test exercises, so the locator must not depend on 
it.
-    this.proxyLoginInput = page.locator('input[placeholder="proxy login"]');
-    this.proxyPasswordInput = page.locator('input[placeholder="proxy 
password"]');
-    this.addButton = page.getByRole('button', { name: 'Add', exact: true });
+    this.repositoryTrigger = page.getByRole('button', { name: 'Repository', 
exact: true });
+    this.createRepositoryTag = 
page.getByTestId('create-interpreter-repository');
+    this.dialog = page.getByRole('dialog').filter({
+      has: page.getByText('Add New Repository', { exact: true })
+    });
+    this.idInput = this.dialog.getByPlaceholder('Repository id', { exact: true 
});
+    this.urlInput = this.dialog.getByPlaceholder('Repository url', { exact: 
true });
+    this.urlProtocol = this.dialog.getByTestId('repository-url-protocol');
+    this.snapshotSwitch = this.dialog.locator('.ant-switch');
+    this.usernameInput = this.dialog.getByPlaceholder('username', { exact: 
true });
+    this.passwordInput = this.dialog.getByPlaceholder('password', { exact: 
true });
+    this.proxyHttpRadio = this.dialog.getByRole('radio', { name: 'HTTP', 
exact: true });
+    this.proxyHttpsRadio = this.dialog.getByRole('radio', { name: 'HTTPS', 
exact: true });
+    this.proxyHostInput = this.dialog.getByPlaceholder('proxy host', { exact: 
true });
+    this.proxyPortInput = this.dialog.getByPlaceholder('proxy port', { exact: 
true });
+    // The proxy binding is under test, so locate it by its user-facing 
placeholder.
+    this.proxyLoginInput = this.dialog.getByPlaceholder('proxy login', { 
exact: true });
+    this.proxyPasswordInput = this.dialog.getByPlaceholder('proxy password', { 
exact: true });
+    this.addButton = this.dialog.getByRole('button', { name: 'Add', exact: 
true });
+    this.dismissButton = this.dialog.getByRole('button', { name: 'Cancel', 
exact: true });
   }
 
   async navigate(): Promise<void> {
@@ -52,6 +73,15 @@ export class InterpreterRepositoryModal extends BasePage {
     await this.idInput.waitFor({ state: 'visible' });
   }
 
+  async selectUrlProtocol(protocol: 'http://' | 'https://' | 'file://'): 
Promise<void> {
+    await this.urlProtocol.click();
+    await 
this.page.locator('.ant-select-dropdown:visible').getByText(protocol, { exact: 
true }).click();
+  }
+
+  async selectProxyProtocol(protocol: 'HTTP' | 'HTTPS'): Promise<void> {
+    await this.dialog.getByText(protocol, { exact: true }).click();
+  }
+
   async fillProxyRepository(form: RepositoryProxyCredentials): Promise<void> {
     await this.fillAndVerifyInput(this.idInput, form.id);
     await this.fillAndVerifyInput(this.urlInput, form.url);
diff --git a/zeppelin-web-angular/e2e/models/interpreter-repository-page.ts 
b/zeppelin-web-angular/e2e/models/interpreter-repository-page.ts
new file mode 100644
index 0000000000..d89c71af17
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/interpreter-repository-page.ts
@@ -0,0 +1,51 @@
+/*
+ * 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 { Locator, Page } from '@playwright/test';
+
+export class InterpreterRepositoryPage {
+  readonly repositoryTrigger: Locator;
+  readonly panel: Locator;
+  readonly heading: Locator;
+  readonly repositoryTags: Locator;
+  readonly createRepositoryTag: Locator;
+  readonly closeIcons: Locator;
+  readonly tooltip: Locator;
+  readonly deleteDialog: Locator;
+  readonly deleteMessage: Locator;
+  readonly confirmDeleteButton: Locator;
+  readonly cancelDeleteButton: Locator;
+
+  constructor(private readonly page: Page) {
+    this.repositoryTrigger = page.getByRole('button', { name: 'Repository', 
exact: true });
+    this.panel = page.getByTestId('interpreter-repositories');
+    this.heading = this.panel.getByRole('heading', { name: 'Repositories', 
exact: true });
+    this.repositoryTags = this.panel.getByTestId('interpreter-repository');
+    this.createRepositoryTag = 
this.panel.getByTestId('create-interpreter-repository');
+    this.closeIcons = page.locator('.anticon-close');
+    this.tooltip = page.locator('.ant-tooltip-inner:visible');
+    this.deleteDialog = page.getByRole('dialog').filter({
+      has: page.getByText('Do you want to delete this repository?', { exact: 
true })
+    });
+    this.deleteMessage = this.deleteDialog.getByText('Do you want to delete 
this repository?', { exact: true });
+    this.confirmDeleteButton = this.deleteDialog.getByRole('button', { name: 
'OK', exact: true });
+    this.cancelDeleteButton = this.deleteDialog.getByRole('button', { name: 
'Cancel', exact: true });
+  }
+
+  repositoryTag(id: string): Locator {
+    return this.repositoryTags.and(this.page.getByText(id, { exact: true }));
+  }
+
+  repositoryCloseIcon(id: string): Locator {
+    return this.repositoryTag(id).locator(this.closeIcons);
+  }
+}
diff --git a/zeppelin-web-angular/e2e/models/interpreter-test.util.ts 
b/zeppelin-web-angular/e2e/models/interpreter-test.util.ts
new file mode 100644
index 0000000000..0442fada52
--- /dev/null
+++ b/zeppelin-web-angular/e2e/models/interpreter-test.util.ts
@@ -0,0 +1,117 @@
+/*
+ * 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, Page, Response, test as base } from '@playwright/test';
+import type { Interpreter, InterpreterMap, InterpreterRepository } from 
'../../src/app/interfaces/interpreter';
+import { waitForZeppelinReady } from '../utils';
+
+export interface InterpreterPageData {
+  settings: Interpreter[];
+  availableInterpreters: InterpreterMap;
+  repositories: Array<Pick<InterpreterRepository, 'id' | 'url'>>;
+}
+
+interface InterpreterWrites {
+  requests: Array<{ method: string; path: string; body: unknown }>;
+  stub(method: 'POST' | 'PUT' | 'DELETE', path: string, responseBody: 
unknown): Promise<void>;
+}
+
+const readBody = async <T>(response: Response): Promise<T> => {
+  expect(response.ok(), `${response.request().method()} ${response.url()} 
returned ${response.status()}`).toBe(true);
+  const payload: { status: string; body: T } = await response.json();
+  expect(payload.status).toBe('OK');
+  return payload.body;
+};
+
+export const openInterpreterPage = async (
+  page: Page,
+  navigation: 'direct' | 'reload' = 'direct'
+): Promise<InterpreterPageData> => {
+  const responseFor = (path: string): Promise<Response> =>
+    page.waitForResponse(
+      response => response.request().method() === 'GET' && new 
URL(response.url()).pathname === path
+    );
+  const settingsResponse = responseFor('/api/interpreter/setting');
+  const availableResponse = responseFor('/api/interpreter');
+  const repositoriesResponse = responseFor('/api/interpreter/repository');
+  if (navigation === 'reload') {
+    await page.reload();
+  } else {
+    await page.goto('/#/interpreter');
+  }
+  await waitForZeppelinReady(page);
+  const [settings, availableInterpreters, repositories] = await Promise.all([
+    settingsResponse.then(response => readBody<Interpreter[]>(response)),
+    availableResponse.then(response => readBody<InterpreterMap>(response)),
+    repositoriesResponse.then(response => 
readBody<InterpreterPageData['repositories']>(response))
+  ]);
+  expect(settings, 'The E2E server must provide at least one interpreter 
setting').not.toEqual([]);
+  return { settings, availableInterpreters, repositories };
+};
+
+export const interpreterTitlePattern = (setting: Interpreter): RegExp => {
+  const escape = (text: string): string => text.replace(/[.*+?^${}()|[\]\\]/g, 
'\\$&');
+  const aliases = setting.interpreterGroup.map((group, index) =>
+    escape(index === 0 ? `%${setting.name}` : `%${setting.name}.${group.name}`)
+  );
+  return new 
RegExp(`^\\s*${escape(setting.name)}\\s+${aliases.join('\\s*,\\s*')}\\s*$`);
+};
+
+export const stubInterpreterRead = async (page: Page, path: string, body: 
unknown): Promise<void> => {
+  await page.route(`**${path}`, async route => {
+    if (route.request().method() === 'GET') {
+      await route.fulfill({ json: { status: 'OK', message: '', body } });
+    } else {
+      await route.fallback();
+    }
+  });
+};
+
+export const test = base.extend<{ interpreterWrites: InterpreterWrites }>({
+  interpreterWrites: [
+    async ({ page }, use) => {
+      const requests: InterpreterWrites['requests'] = [];
+      const unexpectedWrites: string[] = [];
+      await page.route(/\/api\/interpreter(?:\/|$)/, async route => {
+        const request = route.request();
+        if (request.method() === 'GET') {
+          await route.fallback();
+        } else {
+          unexpectedWrites.push(`${request.method()} ${new 
URL(request.url()).pathname}`);
+          await route.abort();
+        }
+      });
+      await use({
+        requests,
+        stub: async (method, path, responseBody) => {
+          await page.route(`**${path}`, async route => {
+            const request = route.request();
+            if (request.method() !== method) {
+              await route.fallback();
+              return;
+            }
+            requests.push({
+              method: request.method(),
+              path: new URL(request.url()).pathname,
+              body: request.postDataJSON()
+            });
+            await route.fulfill({ json: { status: 'OK', message: '', body: 
responseBody } });
+          });
+        }
+      });
+      expect(unexpectedWrites, 'Only explicitly stubbed interpreter writes are 
allowed').toEqual([]);
+    },
+    { auto: true }
+  ]
+});
+
+export { expect };
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/interpreter/create-repository-proxy-credentials.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/create-repository-proxy-credentials.spec.ts
index ec156dac60..a63c7fe064 100644
--- 
a/zeppelin-web-angular/e2e/tests/workspace/interpreter/create-repository-proxy-credentials.spec.ts
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/create-repository-proxy-credentials.spec.ts
@@ -10,9 +10,9 @@
  * limitations under the License.
  */
 
-import { expect, test } from '@playwright/test';
+import { expect, openInterpreterPage, test } from 
'../../../models/interpreter-test.util';
 import { InterpreterRepositoryModal } from 
'../../../models/interpreter-repository-modal';
-import { addPageAnnotationBeforeEach, waitForZeppelinReady, PAGES } from 
'../../../utils';
+import { addPageAnnotationBeforeEach, PAGES } from '../../../utils';
 
 interface AddRepositoryRequestBody {
   id: string;
@@ -23,28 +23,13 @@ interface AddRepositoryRequestBody {
 test.describe('Interpreter Repository - Proxy Credentials', () => {
   addPageAnnotationBeforeEach(PAGES.WORKSPACE.INTERPRETER_CREATE_REPO);
 
-  test('submits the proxy login and proxy password to their own fields', async 
({ page }) => {
+  test('submits the proxy login and proxy password to their own fields', async 
({ page, interpreterWrites }) => {
     const proxyLogin = 'proxy-user';
     const proxyPassword = 'proxy-secret';
     const repoId = `e2e-proxy-repo-${Date.now()}`;
 
-    await page.goto('/#/interpreter');
-    await waitForZeppelinReady(page);
-
-    // Intercept the POST so the payload can be checked without persisting a 
repo.
-    let requestBody: AddRepositoryRequestBody | null = null;
-    await page.route('**/api/interpreter/repository', async route => {
-      if (route.request().method() === 'POST') {
-        requestBody = route.request().postDataJSON() as 
AddRepositoryRequestBody;
-        await route.fulfill({
-          status: 200,
-          contentType: 'application/json',
-          body: JSON.stringify({ status: 'OK', message: '', body: '' })
-        });
-        return;
-      }
-      await route.continue();
-    });
+    await openInterpreterPage(page);
+    await interpreterWrites.stub('POST', '/api/interpreter/repository', '');
 
     const modal = new InterpreterRepositoryModal(page);
     await modal.openCreateModal();
@@ -56,11 +41,12 @@ test.describe('Interpreter Repository - Proxy Credentials', 
() => {
     });
     await modal.submit();
 
-    await expect.poll(() => requestBody, { timeout: 15000 }).not.toBeNull();
+    await expect.poll(() => interpreterWrites.requests.length).toBe(1);
+    const requestBody = interpreterWrites.requests[0].body as 
AddRepositoryRequestBody;
 
     // Regression guard for ZEPPELIN-6520: Password was bound to proxyLogin, 
so the
     // password overwrote the login and proxyPassword was always empty.
-    expect(requestBody!.proxyLogin).toBe(proxyLogin);
-    expect(requestBody!.proxyPassword).toBe(proxyPassword);
+    expect(requestBody.proxyLogin).toBe(proxyLogin);
+    expect(requestBody.proxyPassword).toBe(proxyPassword);
   });
 });
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-page-structure.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-page-structure.spec.ts
new file mode 100644
index 0000000000..db50f87aab
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-page-structure.spec.ts
@@ -0,0 +1,82 @@
+/*
+ * 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, interpreterTitlePattern, openInterpreterPage, test } from 
'../../../models/interpreter-test.util';
+import { InterpreterPage } from '../../../models/interpreter-page';
+import { addPageAnnotationBeforeEach, PAGES } from '../../../utils';
+
+test.describe('Interpreter Page - Real Route Baseline', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.INTERPRETER);
+
+  let interpreterPage: InterpreterPage;
+
+  test.beforeEach(async ({ page }) => {
+    interpreterPage = new InterpreterPage(page);
+  });
+
+  test('loads server settings and primary controls on direct navigation and 
reload', async ({ page }) => {
+    for (const navigation of ['direct', 'reload'] as const) {
+      await test.step(`When opening the interpreter route via ${navigation}`, 
async () => {
+        const data = await openInterpreterPage(page, navigation);
+        await expect(page).toHaveURL(/\/#\/interpreter$/);
+        await expect(interpreterPage.title).toBeVisible();
+        await expect(interpreterPage.description).toBeVisible();
+        await expect(interpreterPage.search).toBeVisible();
+        await expect(interpreterPage.repositoryButton).toBeEnabled();
+        await expect(interpreterPage.createCard).toBeVisible();
+        await 
expect(interpreterPage.settings).toHaveCount(data.settings.length);
+        await 
expect(interpreterPage.settingTitles).toHaveText(data.settings.map(interpreterTitlePattern));
+        for (const setting of data.settings) {
+          const item = interpreterPage.setting(setting.name);
+          await expect(item.edit).toBeEnabled();
+          await expect(item.restart).toBeEnabled();
+          await expect(item.remove).toBeEnabled();
+          await expect(item.existingProcess).toBeDisabled();
+          await expect(item.setPermission).toBeDisabled();
+          await expect(item.bindingMode).toHaveAttribute('disabled', '');
+          await expect(item.save).toHaveCount(0);
+        }
+      });
+    }
+  });
+
+  test('filters actual server settings case-insensitively and restores them on 
clear', async ({ page }) => {
+    const { settings } = await openInterpreterPage(page);
+    const searchable = settings.find(setting => /[a-z]/i.test(setting.name));
+    if (!searchable) {
+      throw new Error('The E2E server must provide a setting name with letters 
to verify case-insensitive search');
+    }
+    const character = searchable.name.match(/[a-z]/i)![0];
+    const query = character === character.toUpperCase() ? 
character.toLowerCase() : character.toUpperCase();
+    const matches = settings.filter(setting => 
setting.name.toLowerCase().includes(query.toLowerCase()));
+    let missingName = 'e2e-no-matching-interpreter';
+    while (settings.some(setting => 
setting.name.toLowerCase().includes(missingName))) {
+      missingName += '-';
+    }
+
+    await test.step('When part of a returned name is searched with different 
casing', async () => {
+      await interpreterPage.search.fill(query);
+      await 
expect(interpreterPage.settingTitles).toHaveText(matches.map(interpreterTitlePattern));
+    });
+
+    await test.step('Then an absent name hides settings without hiding 
Create', async () => {
+      await interpreterPage.search.fill(missingName);
+      await expect(interpreterPage.settings).toHaveCount(0);
+      await expect(interpreterPage.createCard).toBeVisible();
+    });
+
+    await test.step('Then clearing the search restores all server settings', 
async () => {
+      await interpreterPage.search.clear();
+      await 
expect(interpreterPage.settingTitles).toHaveText(settings.map(interpreterTitlePattern));
+    });
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-repositories.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-repositories.spec.ts
new file mode 100644
index 0000000000..7a895342ad
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-repositories.spec.ts
@@ -0,0 +1,397 @@
+/*
+ * 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, openInterpreterPage, stubInterpreterRead, test } from 
'../../../models/interpreter-test.util';
+import { InterpreterRepositoryModal } from 
'../../../models/interpreter-repository-modal';
+import { InterpreterRepositoryPage } from 
'../../../models/interpreter-repository-page';
+import { addPageAnnotationBeforeEach, PAGES } from '../../../utils';
+
+type Repository = { id: string; url: string };
+
+const unusedRepositoryId = (repositories: Repository[], name: string): string 
=> {
+  let id = name;
+  while (repositories.some(repository => repository.id === id)) {
+    id += '-unused';
+  }
+  return id;
+};
+
+const removableRepositories = (repositories: Repository[]) => {
+  const target = {
+    id: unusedRepositoryId(repositories, 'e2e-delete-target'),
+    url: 'https://delete-target.example.test/maven/'
+  };
+  const retained = {
+    id: unusedRepositoryId([...repositories, target], 'e2e-delete-retained'),
+    url: 'https://delete-retained.example.test/maven/'
+  };
+  return { target, repositories: [...repositories, target, retained] };
+};
+
+const submissionCases = [
+  {
+    name: 'HTTP with snapshots disabled',
+    protocol: 'http://' as const,
+    id: 'http-repository',
+    url: 'http.example.test/maven/',
+    expectedUrl: 'http://http.example.test/maven/',
+    snapshot: false,
+    username: '',
+    password: '',
+    proxyProtocol: 'HTTP',
+    proxyHost: '',
+    proxyPort: null
+  },
+  {
+    name: 'HTTPS with snapshots, repository credentials and a proxy',
+    protocol: 'https://' as const,
+    id: 'https-repository',
+    url: 'secure.example.test/maven/',
+    expectedUrl: 'https://secure.example.test/maven/',
+    snapshot: true,
+    username: 'repository-user',
+    password: 'repository-secret',
+    proxyProtocol: 'HTTPS',
+    proxyHost: 'proxy.example.test',
+    proxyPort: '8080'
+  },
+  {
+    name: 'a file URL',
+    protocol: 'file://' as const,
+    id: 'file-repository',
+    url: '/tmp/zeppelin-file-repository/',
+    expectedUrl: 'file:///tmp/zeppelin-file-repository/',
+    snapshot: false,
+    username: '',
+    password: '',
+    proxyProtocol: 'HTTP',
+    proxyHost: '',
+    proxyPort: null
+  }
+];
+
+test.describe('Interpreter repositories', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.INTERPRETER);
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.INTERPRETER_CREATE_REPO);
+
+  let repositoryPage: InterpreterRepositoryPage;
+  let modal: InterpreterRepositoryModal;
+  let repositories: Repository[];
+
+  test.beforeEach(async ({ page }) => {
+    repositoryPage = new InterpreterRepositoryPage(page);
+    modal = new InterpreterRepositoryModal(page);
+
+    await test.step('Given the interpreter page loads the real server 
repository list', async () => {
+      ({ repositories } = await openInterpreterPage(page));
+      await expect(repositoryPage.repositoryTrigger).toBeVisible();
+    });
+  });
+
+  test('opens and closes the repository panel with protected defaults and 
server repository tooltips', async ({
+    interpreterWrites
+  }) => {
+    await test.step('Given the repository panel is initially closed', async () 
=> {
+      await expect(repositoryPage.panel).toBeHidden();
+    });
+
+    await test.step('When the Repository button is selected', async () => {
+      await repositoryPage.repositoryTrigger.click();
+    });
+
+    await test.step('Then all server repositories are shown and defaults have 
no remove control', async () => {
+      await expect(repositoryPage.heading).toBeVisible();
+      await 
expect(repositoryPage.repositoryTags).toHaveText(repositories.map(repository => 
repository.id));
+      await expect(repositoryPage.repositoryTag('central')).toBeVisible();
+      await expect(repositoryPage.repositoryTag('local')).toBeVisible();
+      await 
expect(repositoryPage.repositoryCloseIcon('central')).toHaveCount(0);
+      await expect(repositoryPage.repositoryCloseIcon('local')).toHaveCount(0);
+      await expect(repositoryPage.createRepositoryTag).toBeVisible();
+    });
+
+    await test.step('Then each repository tooltip shows its URL', async () => {
+      await expect(repositoryPage.panel).toHaveCSS('opacity', '1');
+      for (const repository of repositories) {
+        await repositoryPage.repositoryTag(repository.id).hover();
+        await expect(repositoryPage.tooltip).toHaveText(repository.url);
+      }
+    });
+
+    await test.step('When the Repository button is selected again', async () 
=> {
+      await repositoryPage.repositoryTrigger.click();
+    });
+
+    await test.step('Then the panel closes without changing repositories', 
async () => {
+      await expect(repositoryPage.panel).toBeHidden();
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+
+  test('cancels deleting a custom repository without sending a mutation', 
async ({ page, interpreterWrites }) => {
+    const deletion = removableRepositories(repositories);
+    await stubInterpreterRead(page, '/api/interpreter/repository', 
deletion.repositories);
+    await openInterpreterPage(page, 'reload');
+
+    await test.step('Given the repository panel is open', async () => {
+      await repositoryPage.repositoryTrigger.click();
+      await 
expect(repositoryPage.repositoryTags).toHaveText(deletion.repositories.map(repository
 => repository.id));
+      await expect(repositoryPage.panel).toHaveCSS('opacity', '1');
+    });
+
+    await test.step('When the custom repository remove control is selected', 
async () => {
+      await repositoryPage.repositoryCloseIcon(deletion.target.id).click();
+    });
+
+    await test.step('Then confirmation identifies the repository before any 
mutation', async () => {
+      await expect(repositoryPage.deleteDialog).toBeVisible();
+      await 
expect(repositoryPage.deleteDialog).toContainText(deletion.target.id);
+      await expect(repositoryPage.deleteMessage).toBeVisible();
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+
+    await test.step('When deletion is cancelled', async () => {
+      await repositoryPage.cancelDeleteButton.click();
+    });
+
+    await test.step('Then every repository remains and no mutation was sent', 
async () => {
+      await expect(repositoryPage.deleteDialog).toBeHidden();
+      await 
expect(repositoryPage.repositoryTags).toHaveText(deletion.repositories.map(repository
 => repository.id));
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+
+  test('confirms deletion with the repository ID and removes only the selected 
tag', async ({
+    page,
+    interpreterWrites
+  }) => {
+    const deletion = removableRepositories(repositories);
+    await stubInterpreterRead(page, '/api/interpreter/repository', 
deletion.repositories);
+    await openInterpreterPage(page, 'reload');
+
+    await test.step('Given deletion is permitted only for the selected 
repository', async () => {
+      await interpreterWrites.stub('DELETE', 
`/api/interpreter/repository/${deletion.target.id}`, '');
+      await repositoryPage.repositoryTrigger.click();
+      await 
expect(repositoryPage.repositoryTags).toHaveText(deletion.repositories.map(repository
 => repository.id));
+      await expect(repositoryPage.panel).toHaveCSS('opacity', '1');
+    });
+
+    await test.step('When deletion of the custom repository is confirmed', 
async () => {
+      await repositoryPage.repositoryCloseIcon(deletion.target.id).click();
+      await expect(repositoryPage.deleteMessage).toBeVisible();
+      await 
expect(repositoryPage.deleteDialog).toContainText(deletion.target.id);
+      expect(interpreterWrites.requests).toEqual([]);
+      await repositoryPage.confirmDeleteButton.click();
+    });
+
+    await test.step('Then exactly that repository is deleted and the other 
tags remain', async () => {
+      await expect(repositoryPage.deleteDialog).toBeHidden();
+      await 
expect(repositoryPage.repositoryTag(deletion.target.id)).toHaveCount(0);
+      await expect(repositoryPage.repositoryTags).toHaveText(
+        deletion.repositories
+          .filter(repository => repository.id !== deletion.target.id)
+          .map(repository => repository.id)
+      );
+      expect(interpreterWrites.requests).toHaveLength(1);
+      expect(interpreterWrites.requests[0]).toMatchObject({
+        method: 'DELETE',
+        path: `/api/interpreter/repository/${deletion.target.id}`
+      });
+    });
+  });
+
+  test('requires both repository ID and URL before enabling Add', async ({ 
interpreterWrites }) => {
+    await test.step('When the add repository modal is opened', async () => {
+      await modal.openCreateModal();
+    });
+
+    await test.step('Then the empty required fields disable Add and defaults 
are visible', async () => {
+      await expect(modal.dialog).toBeVisible();
+      await expect(modal.idInput).toHaveValue('');
+      await expect(modal.urlInput).toHaveValue('');
+      await expect(modal.urlProtocol).toHaveText('http://');
+      await expect(modal.snapshotSwitch).not.toHaveClass(/ant-switch-checked/);
+      await expect(modal.proxyHttpRadio).toBeChecked();
+      await expect(modal.proxyLoginInput).toBeVisible();
+      await expect(modal.proxyPasswordInput).toHaveAttribute('type', 
'password');
+      await expect(modal.addButton).toBeDisabled();
+    });
+
+    await test.step('When only the ID is supplied', async () => {
+      await modal.idInput.fill(unusedRepositoryId(repositories, 
'required-fields-repository'));
+    });
+
+    await test.step('Then Add remains disabled until a URL is also supplied', 
async () => {
+      await expect(modal.addButton).toBeDisabled();
+      await modal.urlInput.fill('required.example.test/maven/');
+      await expect(modal.addButton).toBeEnabled();
+    });
+
+    await test.step('When the ID is removed while the URL remains', async () 
=> {
+      await modal.idInput.fill('');
+    });
+
+    await test.step('Then a URL alone cannot enable Add or send a mutation', 
async () => {
+      await expect(modal.addButton).toBeDisabled();
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+
+  test('rejects an out-of-range proxy port and accepts port 8080', async ({ 
interpreterWrites }) => {
+    await test.step('Given the required repository fields are complete', async 
() => {
+      await modal.openCreateModal();
+      await modal.idInput.fill(unusedRepositoryId(repositories, 
'proxy-port-repository'));
+      await modal.urlInput.fill('port.example.test/maven/');
+      await expect(modal.addButton).toBeEnabled();
+    });
+
+    await test.step('When proxy port 99999 is entered', async () => {
+      await modal.proxyPortInput.fill('99999');
+    });
+
+    await test.step('Then Add is disabled without submitting', async () => {
+      await expect(modal.addButton).toBeDisabled();
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+
+    await test.step('When the proxy port is corrected to 8080', async () => {
+      await modal.proxyPortInput.fill('8080');
+    });
+
+    await test.step('Then Add is enabled without sending a mutation', async () 
=> {
+      await expect(modal.addButton).toBeEnabled();
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+
+  for (const submission of submissionCases) {
+    test(`submits ${submission.name} and renders the refreshed server 
repository list`, async ({
+      page,
+      interpreterWrites
+    }) => {
+      const submittedId = unusedRepositoryId(repositories, submission.id);
+      const serverRepository = {
+        id: unusedRepositoryId(
+          [...repositories, { id: submittedId, url: submission.expectedUrl }],
+          'server-only-result'
+        ),
+        url: `https://server.example.test/${submission.id}/`
+      };
+
+      await test.step('Given the add repository modal is open with the initial 
repository list', async () => {
+        await modal.openCreateModal();
+        await 
expect(repositoryPage.repositoryTags).toHaveText(repositories.map(repository => 
repository.id));
+        await interpreterWrites.stub('POST', '/api/interpreter/repository', 
'');
+      });
+
+      await test.step('When the repository URL and optional settings are 
entered', async () => {
+        await modal.idInput.fill(submittedId);
+        await expect(modal.idInput).toHaveValue(submittedId);
+        await modal.urlInput.fill(submission.url);
+        await expect(modal.idInput).toHaveValue(submittedId);
+        await modal.selectUrlProtocol(submission.protocol);
+        await expect(modal.urlProtocol).toHaveText(submission.protocol);
+        await 
expect(modal.snapshotSwitch).not.toHaveClass(/ant-switch-checked/);
+        if (submission.snapshot) {
+          await modal.snapshotSwitch.click();
+        }
+        await expect(modal.snapshotSwitch).toHaveClass(
+          submission.snapshot ? /ant-switch-checked/ : 
/^(?!.*ant-switch-checked)/
+        );
+        await modal.usernameInput.fill(submission.username);
+        await modal.passwordInput.fill(submission.password);
+        await expect(modal.passwordInput).toHaveAttribute('type', 'password');
+        if (submission.proxyProtocol === 'HTTPS') {
+          await modal.selectProxyProtocol('HTTPS');
+        }
+        await expect(modal.proxyHttpsRadio).toBeChecked({ checked: 
submission.proxyProtocol === 'HTTPS' });
+        await expect(modal.proxyHttpRadio).toBeChecked({ checked: 
submission.proxyProtocol === 'HTTP' });
+        await modal.proxyHostInput.fill(submission.proxyHost);
+        if (submission.proxyPort !== null) {
+          await modal.proxyPortInput.fill(submission.proxyPort);
+        }
+        await expect(modal.addButton).toBeEnabled();
+      });
+
+      await test.step('When Add succeeds and the next repository GET returns a 
new server-only tag', async () => {
+        await stubInterpreterRead(page, '/api/interpreter/repository', 
[...repositories, serverRepository]);
+        const refreshedRepositories = page.waitForResponse(
+          response =>
+            response.request().method() === 'GET' && new 
URL(response.url()).pathname === '/api/interpreter/repository'
+        );
+        await modal.submit();
+        await refreshedRepositories;
+      });
+
+      await test.step('Then the independently expected payload is sent and the 
refreshed response is rendered', async () => {
+        await expect(modal.dialog).toBeHidden();
+        expect(interpreterWrites.requests).toEqual([
+          {
+            method: 'POST',
+            path: '/api/interpreter/repository',
+            body: {
+              id: submittedId,
+              url: submission.expectedUrl,
+              snapshot: submission.snapshot,
+              username: submission.username,
+              password: submission.password,
+              proxyProtocol: submission.proxyProtocol,
+              proxyHost: submission.proxyHost,
+              proxyPort: submission.proxyPort,
+              proxyLogin: '',
+              proxyPassword: ''
+            }
+          }
+        ]);
+        await expect(repositoryPage.repositoryTags).toHaveText([
+          ...repositories.map(repository => repository.id),
+          serverRepository.id
+        ]);
+        await expect(repositoryPage.repositoryTag(submittedId)).toHaveCount(0);
+        await expect(repositoryPage.panel).toHaveCSS('opacity', '1');
+        await repositoryPage.repositoryTag(serverRepository.id).hover();
+        await expect(repositoryPage.tooltip).toHaveText(serverRepository.url);
+      });
+    });
+  }
+
+  test('cancels a completed add form without changing repositories or sending 
a mutation', async ({
+    interpreterWrites
+  }) => {
+    const cancelledId = unusedRepositoryId(repositories, 
'cancelled-repository');
+
+    await test.step('Given a valid add repository form has unsaved changes', 
async () => {
+      await modal.openCreateModal();
+      await 
expect(repositoryPage.repositoryTags).toHaveText(repositories.map(repository => 
repository.id));
+      await modal.idInput.fill(cancelledId);
+      await modal.urlInput.fill('cancelled.example.test/maven/');
+      await modal.selectUrlProtocol('https://');
+      await modal.snapshotSwitch.click();
+      await modal.usernameInput.fill('unsaved-user');
+      await modal.passwordInput.fill('unsaved-password');
+      await modal.selectProxyProtocol('HTTPS');
+      await modal.proxyHostInput.fill('unsaved-proxy.example.test');
+      await modal.proxyPortInput.fill('8080');
+      await expect(modal.addButton).toBeEnabled();
+    });
+
+    await test.step('When Cancel is selected', async () => {
+      await modal.dismissButton.click();
+    });
+
+    await test.step('Then the modal closes and the original repositories 
remain without a mutation', async () => {
+      await expect(modal.dialog).toBeHidden();
+      await 
expect(repositoryPage.repositoryTags).toHaveText(repositories.map(repository => 
repository.id));
+      await expect(repositoryPage.repositoryTag(cancelledId)).toHaveCount(0);
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+});
diff --git 
a/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-settings.spec.ts
 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-settings.spec.ts
new file mode 100644
index 0000000000..a9aa285e38
--- /dev/null
+++ 
b/zeppelin-web-angular/e2e/tests/workspace/interpreter/interpreter-settings.spec.ts
@@ -0,0 +1,563 @@
+/*
+ * 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 type { Interpreter, InterpreterPropertyValue } from 
'../../../../src/app/interfaces/interpreter';
+import {
+  expect,
+  InterpreterPageData,
+  interpreterTitlePattern,
+  openInterpreterPage,
+  stubInterpreterRead,
+  test
+} from '../../../models/interpreter-test.util';
+import { InterpreterItemPage, InterpreterPage } from 
'../../../models/interpreter-page';
+import { addPageAnnotationBeforeEach, PAGES } from '../../../utils';
+
+const unusedName = (names: string[], prefix: string): string => {
+  let name = prefix;
+  while (names.includes(name)) {
+    name += '-new';
+  }
+  return name;
+};
+
+const typedSetting = (source: Interpreter): Interpreter => ({
+  ...structuredClone(source),
+  id: `${source.name}-different-id`,
+  status: 'READY',
+  option: {
+    remote: true,
+    port: -1,
+    host: '',
+    owners: [],
+    perNote: 'shared',
+    perUser: 'shared',
+    isExistingProcess: false,
+    setPermission: false,
+    isUserImpersonate: false
+  },
+  properties: {
+    greeting: { name: 'greeting', value: 'hello from server', type: 'string' },
+    timeout: { name: 'timeout', value: '1000', type: 'number' },
+    enabled: { name: 'enabled', value: false, type: 'checkbox' },
+    secret: { name: 'secret', value: 'fixture-secret', type: 'password' },
+    website: { name: 'website', value: 'https://example.org/', type: 'url' },
+    script: { name: 'script', value: 'line one\nline two', type: 'textarea' }
+  },
+  dependencies: [
+    { groupArtifactVersion: 'org.example:original:1.0', exclusions: 
['org.example:excluded'], local: false }
+  ]
+});
+
+const creationDefaults = (available: Interpreter[], group: string): 
Record<string, InterpreterPropertyValue> => {
+  const properties: Record<string, InterpreterPropertyValue> = {};
+  for (const interpreter of available.filter(entry => entry.group === group)) {
+    for (const [name, property] of Object.entries(interpreter.properties)) {
+      const value = property.defaultValue ?? '';
+      properties[name] = {
+        name,
+        type: property.type,
+        value: property.type === 'checkbox' ? value === true || value === 
'true' : String(value)
+      };
+    }
+  }
+  return properties;
+};
+
+const creationGroup = (available: Interpreter[]): Interpreter => {
+  const group = available.find(entry => Object.keys(entry.properties).length > 
0);
+  if (!group) {
+    throw new Error('The E2E server must provide an available interpreter 
group with default properties');
+  }
+  return group;
+};
+
+test.describe('Interpreter Settings', () => {
+  addPageAnnotationBeforeEach(PAGES.WORKSPACE.INTERPRETER_ITEM);
+
+  let interpreterPage: InterpreterPage;
+  let item: InterpreterItemPage;
+  let serverData: InterpreterPageData;
+  let setting: Interpreter;
+
+  test.beforeEach(async ({ page }) => {
+    interpreterPage = new InterpreterPage(page);
+    serverData = await openInterpreterPage(page);
+    setting = serverData.settings[0];
+    item = interpreterPage.setting(setting.name);
+    await 
expect(interpreterPage.settingTitles).toHaveText(serverData.settings.map(interpreterTitlePattern));
+  });
+
+  test('shows typed values, ready and error states when those settings are 
returned', async ({ page }) => {
+    const ready = typedSetting(setting);
+    const failed = {
+      ...structuredClone(setting),
+      name: unusedName(
+        serverData.settings.map(entry => entry.name),
+        'e2e-failed-setting'
+      ),
+      status: 'ERROR',
+      errorReason: 'Fixture dependency could not be downloaded'
+    };
+    await stubInterpreterRead(page, '/api/interpreter/setting', [ready, 
...serverData.settings.slice(1), failed]);
+    await openInterpreterPage(page, 'reload');
+
+    await test.step('Then view mode renders each property type and disables 
editing', async () => {
+      await expect(item.card.getByRole('heading', { name: 'Option', exact: 
true })).toBeVisible();
+      await expect(item.card.getByRole('heading', { name: 'Properties', exact: 
true })).toBeVisible();
+      await expect(item.card.getByRole('heading', { name: 'Dependencies', 
exact: true })).toBeVisible();
+      await expect(item.propertyCell('greeting')).toHaveText('hello from 
server');
+      await expect(item.propertyCell('timeout')).toHaveText('1000');
+      await expect(item.propertyCell('enabled')).toHaveText('false');
+      await expect(item.propertyCell('secret')).toHaveText('******');
+      await 
expect(item.propertyCell('website').getByRole('link')).toHaveAttribute('href', 
'https://example.org/');
+      await expect(item.propertyCell('script')).toHaveText('line one line 
two');
+      await 
expect(item.dependencyRow('org.example:original:1.0')).toContainText('org.example:excluded');
+      await expect(item.properties.getByText('Action', { exact: true 
})).toHaveCount(0);
+    });
+
+    await test.step('Then the returned status and error reason are visible', 
async () => {
+      await item.status.hover();
+      await expect(item.tooltip).toHaveText('Ready');
+      const errorItem = interpreterPage.setting(failed.name);
+      await errorItem.status.hover();
+      await expect(item.tooltip).toHaveText('Error downloading dependencies');
+      await expect(errorItem.card.getByText(failed.errorReason, { exact: true 
})).toBeVisible();
+    });
+  });
+
+  test('validates a new name against actual settings and populates an 
available group', async ({
+    interpreterWrites
+  }) => {
+    const available = Object.values(serverData.availableInterpreters);
+    const group = creationGroup(available);
+    const defaults = creationDefaults(available, group.group);
+    const form = interpreterPage.createForm();
+    const name = unusedName(
+      serverData.settings.map(entry => entry.name),
+      'e2e-new-interpreter'
+    );
+
+    await test.step('When Create opens a blank form', async () => {
+      await interpreterPage.createCard.click();
+      await expect(form.card.getByRole('heading', { name: 'Create new 
interpreter', exact: true })).toBeVisible();
+      await expect(interpreterPage.createCard).toHaveCount(0);
+      await expect(form.save).toBeDisabled();
+      await form.name.fill(name);
+      await expect(form.save).toBeDisabled();
+    });
+
+    await test.step('Then a real available group supplies property rows and 
existing names are rejected', async () => {
+      await form.selectGroup(group.name);
+      await expect(form.card.getByRole('button', { name: 'Remove property', 
exact: true })).toHaveCount(
+        Object.keys(defaults).length
+      );
+      for (const key of Object.keys(defaults)) {
+        await expect(form.propertyRow(key)).toBeVisible();
+      }
+      await expect(form.save).toBeEnabled();
+      await form.name.fill(setting.name);
+      await form.name.blur();
+      await expect(form.card.getByText(`Name '${setting.name}' already 
exists`, { exact: true })).toBeVisible();
+      await expect(form.save).toBeDisabled();
+      await form.name.clear();
+      await expect(form.save).toBeDisabled();
+    });
+
+    await test.step('Then Cancel restores Create without changing the 
server-backed list or writing', async () => {
+      await form.cancel.click();
+      await expect(form.card).toHaveCount(0);
+      await expect(interpreterPage.createCard).toBeVisible();
+      await 
expect(interpreterPage.settingTitles).toHaveText(serverData.settings.map(interpreterTitlePattern));
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+
+  test('submits real group defaults and renders the independently stubbed 
creation response', async ({
+    interpreterWrites
+  }) => {
+    const available = Object.values(serverData.availableInterpreters);
+    const group = creationGroup(available);
+    const name = unusedName(
+      serverData.settings.map(entry => entry.name),
+      'e2e-created-interpreter'
+    );
+    const created: Interpreter = {
+      ...structuredClone(setting),
+      id: 'created-server-id',
+      name,
+      group: group.group,
+      properties: {
+        'e2e.server.response': { name: 'e2e.server.response', type: 'string', 
value: 'from mutation response' }
+      }
+    };
+    await interpreterWrites.stub('POST', '/api/interpreter/setting', created);
+    const form = interpreterPage.createForm();
+
+    await test.step('When a real available group and an additional property 
are submitted', async () => {
+      await interpreterPage.createCard.click();
+      await form.name.fill(name);
+      await form.selectGroup(group.name);
+      await form.propertyKey.fill('e2e.entered.value');
+      await form.propertyValue.fill('entered in browser');
+      await form.card.getByRole('button', { name: 'Add property', exact: true 
}).click();
+      await form.save.click();
+    });
+
+    await test.step('Then the request contains the real defaults and the new 
card uses the response', async () => {
+      await 
expect(interpreterPage.setting(name).propertyCell('e2e.server.response')).toHaveText(
+        'from mutation response'
+      );
+      await expect(form.card).toHaveCount(0);
+      await expect(interpreterPage.createCard).toBeVisible();
+      await 
expect(interpreterPage.settings).toHaveCount(serverData.settings.length + 1);
+      expect(interpreterWrites.requests).toEqual([
+        {
+          method: 'POST',
+          path: '/api/interpreter/setting',
+          body: expect.objectContaining({
+            name,
+            group: group.group,
+            properties: {
+              ...creationDefaults(available, group.group),
+              'e2e.entered.value': { name: 'e2e.entered.value', type: 
'string', value: 'entered in browser' }
+            },
+            dependencies: []
+          })
+        }
+      ]);
+    });
+  });
+
+  test('Cancel restores every typed property on reopening', async ({ page, 
interpreterWrites }) => {
+    const controlled = typedSetting(setting);
+    await stubInterpreterRead(page, '/api/interpreter/setting', [controlled, 
...serverData.settings.slice(1)]);
+    await openInterpreterPage(page, 'reload');
+
+    await test.step('When typed values and process options are changed without 
saving', async () => {
+      await item.edit.click();
+      await expect(item.properties.getByText('Action', { exact: true 
})).toBeVisible();
+      await item.propertyCell('greeting').getByRole('textbox').fill('unsaved 
greeting');
+      await item.propertyCell('timeout').getByRole('spinbutton').fill('1234');
+      await item.propertyCell('enabled').getByRole('checkbox').check();
+      await item.passwordValue('secret').fill('unsaved secret');
+      await 
item.propertyCell('website').getByRole('textbox').fill('https://changed.example.org/');
+      await item.propertyCell('script').getByRole('textbox').fill('unsaved 
script');
+      await item.existingProcess.check();
+      await item.host.fill('unsaved.example.org');
+      await item.cancel.click();
+    });
+
+    await test.step('Then view and reopened edit mode retain the returned 
configuration', async () => {
+      await expect(item.edit).toBeVisible();
+      await expect(item.propertyCell('greeting')).toHaveText('hello from 
server');
+      await item.edit.click();
+      await 
expect(item.propertyCell('greeting').getByRole('textbox')).toHaveValue('hello 
from server');
+      await 
expect(item.propertyCell('timeout').getByRole('spinbutton')).toHaveValue('1000');
+      await 
expect(item.propertyCell('enabled').getByRole('checkbox')).not.toBeChecked();
+      await expect(item.passwordValue('secret')).toHaveValue('fixture-secret');
+      await 
expect(item.propertyCell('website').getByRole('textbox')).toHaveValue('https://example.org/');
+      await 
expect(item.propertyCell('script').getByRole('textbox')).toHaveValue('line 
one\nline two');
+      await expect(item.existingProcess).not.toBeChecked();
+      await expect(item.host).toHaveCount(0);
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+
+  test('reveals and submits process and permission fields on a real setting', 
async ({ interpreterWrites }) => {
+    const owner = unusedName(setting.option.owners ?? [], 'e2e-owner');
+    const path = 
`/api/interpreter/setting/${encodeURIComponent(setting.name)}`;
+    await interpreterWrites.stub('PUT', path, setting);
+
+    await test.step('When options on a returned setting are toggled in edit 
mode', async () => {
+      await item.edit.click();
+      await item.existingProcess.uncheck();
+      await item.setPermission.uncheck();
+      await expect(item.host).toHaveCount(0);
+      await expect(item.port).toHaveCount(0);
+      await expect(item.owners).toHaveCount(0);
+      await item.existingProcess.check();
+      await expect(item.host).toBeVisible();
+      await expect(item.port).toBeVisible();
+      await item.setPermission.check();
+      await expect(item.owners.getByRole('textbox')).toBeVisible();
+      await item.existingProcess.uncheck();
+      await item.setPermission.uncheck();
+      await expect(item.host).toHaveCount(0);
+      await expect(item.port).toHaveCount(0);
+      await expect(item.owners).toHaveCount(0);
+    });
+
+    await test.step('Then the entered host, valid port and owner are submitted 
without persisting', async () => {
+      await item.existingProcess.check();
+      await item.host.fill('worker.example.org');
+      await item.port.fill('99999');
+      await expect(item.save).toBeDisabled();
+      await item.port.fill('8080');
+      await item.setPermission.check();
+      await item.owners.getByRole('textbox').fill(`${owner},`);
+      await expect(item.owners.getByText(owner, { exact: true 
})).toBeVisible();
+      await expect(item.save).toBeEnabled();
+      await item.save.click();
+      await expect(item.edit).toBeVisible();
+      await expect
+        .poll(() => interpreterWrites.requests)
+        .toEqual([
+          {
+            method: 'PUT',
+            path,
+            body: expect.objectContaining({
+              option: expect.objectContaining({
+                isExistingProcess: true,
+                host: 'worker.example.org',
+                port: 8080,
+                setPermission: true,
+                owners: [...(setting.option.owners ?? []), owner]
+              })
+            })
+          }
+        ]);
+    });
+  });
+
+  test('adds, updates and removes properties and dependencies locally', async 
({ page, interpreterWrites }) => {
+    const controlled: Interpreter = {
+      ...structuredClone(setting),
+      dependencies: [
+        { groupArtifactVersion: 'org.example:original:1.0', exclusions: 
['org.example:excluded'], local: false }
+      ]
+    };
+    await stubInterpreterRead(page, '/api/interpreter/setting', [controlled, 
...serverData.settings.slice(1)]);
+    await openInterpreterPage(page, 'reload');
+
+    await test.step('When a property and dependency are added in edit mode', 
async () => {
+      await item.edit.click();
+      await item.propertyKey.fill('custom.property');
+      await item.propertyValue.fill('first value');
+      await item.card.getByRole('button', { name: 'Add property', exact: true 
}).click();
+      await 
expect(item.propertyCell('custom.property').getByRole('textbox')).toHaveValue('first
 value');
+      await item.propertyKey.fill('custom.property');
+      await item.selectPropertyType('number');
+      await item.newProperty.getByRole('spinbutton').fill('42');
+      await item.card.getByRole('button', { name: 'Add property', exact: true 
}).click();
+      await expect(item.propertyRow('custom.property')).toHaveCount(1);
+      await 
expect(item.propertyCell('custom.property').getByRole('spinbutton')).toHaveValue('42');
+      await item.artifact.fill('org.example:added:2.0');
+      await item.exclusions.fill('org.example:omit');
+      await item.card.getByRole('button', { name: 'Add dependency', exact: 
true }).click();
+      await 
expect(item.editableDependencyRow(1).getByRole('textbox').nth(0)).toHaveValue('org.example:added:2.0');
+      await 
expect(item.editableDependencyRow(1).getByRole('textbox').nth(1)).toHaveValue('org.example:omit');
+    });
+
+    await test.step('Then removal discards only added entries without a 
write', async () => {
+      await item.propertyRow('custom.property').getByRole('button', { name: 
'Remove property', exact: true }).click();
+      await expect(item.propertyRow('custom.property')).toHaveCount(0);
+      await item.editableDependencyRow(1).getByRole('button', { name: 'Remove 
dependency', exact: true }).click();
+      await expect(item.dependencies.getByRole('textbox')).toHaveCount(4);
+      await item.cancel.click();
+      await 
expect(item.dependencyRow('org.example:original:1.0')).toContainText('org.example:excluded');
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+  });
+
+  test('updates by name rather than ID with typed properties and edited 
dependencies', async ({
+    page,
+    interpreterWrites
+  }) => {
+    const controlled = typedSetting(setting);
+    await stubInterpreterRead(page, '/api/interpreter/setting', [controlled, 
...serverData.settings.slice(1)]);
+    await openInterpreterPage(page, 'reload');
+    const updated = structuredClone(controlled);
+    updated.properties.greeting.value = 'submitted greeting';
+    const path = 
`/api/interpreter/setting/${encodeURIComponent(controlled.name)}`;
+    await interpreterWrites.stub('PUT', path, updated);
+
+    await test.step('When changed values, an edited dependency and a pending 
addition are saved', async () => {
+      await item.edit.click();
+      await item.propertyCell('greeting').getByRole('textbox').fill('submitted 
greeting');
+      await item.propertyCell('timeout').getByRole('spinbutton').fill('60000');
+      await item.propertyCell('enabled').getByRole('checkbox').check();
+      await 
item.editableDependencyRow(0).getByRole('textbox').nth(0).fill('org.example:edited:4.0');
+      await 
item.editableDependencyRow(0).getByRole('textbox').nth(1).fill('org.example:edited-exclusion');
+      await item.artifact.fill('org.example:new:3.0');
+      await item.exclusions.fill(' org.example:first, , org.example:second ');
+      const response = page.waitForResponse(
+        result => new URL(result.url()).pathname === path && 
result.request().method() === 'PUT'
+      );
+      await item.save.click();
+      await (await response).finished();
+    });
+
+    await test.step('Then submitted values remain editable and the exact 
request targets the name', async () => {
+      await expect(item.propertyCell('greeting')).toHaveText('submitted 
greeting');
+      await item.edit.click();
+      await 
expect(item.propertyCell('greeting').getByRole('textbox')).toHaveValue('submitted
 greeting');
+      expect(interpreterWrites.requests).toEqual([
+        {
+          method: 'PUT',
+          path,
+          body: {
+            option: expect.objectContaining({ perNote: 'shared', perUser: 
'shared', remote: true }),
+            properties: {
+              greeting: { name: 'greeting', type: 'string', value: 'submitted 
greeting' },
+              timeout: { name: 'timeout', type: 'number', value: '60000' },
+              enabled: { name: 'enabled', type: 'checkbox', value: true },
+              secret: { name: 'secret', type: 'password', value: 
'fixture-secret' },
+              website: { name: 'website', type: 'url', value: 
'https://example.org/' },
+              script: { name: 'script', type: 'textarea', value: 'line 
one\nline two' }
+            },
+            dependencies: [
+              { groupArtifactVersion: 'org.example:edited:4.0', exclusions: 
['org.example:edited-exclusion'] },
+              { groupArtifactVersion: 'org.example:new:3.0', exclusions: 
['org.example:first', 'org.example:second'] }
+            ]
+          }
+        }
+      ]);
+    });
+  });
+
+  for (const principal of ['anonymous', 'fixture-user']) {
+    test(`exposes and submits binding choices for an isolated ${principal} 
ticket`, async ({
+      page,
+      interpreterWrites
+    }) => {
+      await page.route('**/api/security/ticket', route =>
+        route.fulfill({
+          json: {
+            status: 'OK',
+            message: '',
+            body: {
+              principal,
+              ticket: principal === 'anonymous' ? 'anonymous' : 
'fixture-ticket',
+              roles: principal === 'anonymous' ? '[]' : '["users"]'
+            }
+          }
+        })
+      );
+      await openInterpreterPage(page, 'reload');
+      await item.edit.click();
+
+      await test.step('When binding choices are shown for the selected 
identity', async () => {
+        await item.bindingMode.click();
+        await expect(item.bindingMenu.getByText('Globally', { exact: true 
})).toBeVisible();
+        await expect(item.bindingMenu.getByText('Per Note', { exact: true 
})).toBeVisible();
+        await expect(item.bindingMenu.getByText('Per User', { exact: true 
})).toHaveCount(
+          principal === 'anonymous' ? 0 : 1
+        );
+        await item.bindingMenu.getByText('Per Note', { exact: true }).click();
+        await expect(item.bindingMode).toContainText('Per Note');
+        await item.card.getByText('scoped', { exact: true }).click();
+        await item.bindingMenu.getByText('isolated per note', { exact: true 
}).click();
+        await expect(item.card.getByText('isolated', { exact: true 
})).toBeVisible();
+      });
+
+      await test.step('Then per-user isolation controls follow the identity 
capability', async () => {
+        if (principal !== 'anonymous') {
+          await item.bindingMode.click();
+          await item.bindingMenu.getByText('Per User', { exact: true 
}).click();
+          await item.card.getByText('scoped', { exact: true }).click();
+          await item.bindingMenu.getByText('isolated per user', { exact: true 
}).click();
+        }
+        await expect(item.card.getByRole('checkbox', { name: 'User 
Impersonate', exact: true })).toHaveCount(
+          principal === 'anonymous' ? 0 : 1
+        );
+        await expect(item.card.getByRole('button', { name: 'Add per-note 
process', exact: true })).toHaveCount(
+          principal === 'anonymous' ? 0 : 1
+        );
+      });
+
+      await test.step('Then isolation and optional impersonation are sent in 
the intercepted request', async () => {
+        if (principal !== 'anonymous') {
+          await item.card.getByRole('checkbox', { name: 'User Impersonate', 
exact: true }).check();
+          await item.card.getByRole('button', { name: 'Add per-note process', 
exact: true }).click();
+        }
+        await expect(item.card.getByRole('button', { name: 'Remove per-note 
process', exact: true })).toHaveCount(
+          principal === 'anonymous' ? 0 : 1
+        );
+        if (principal !== 'anonymous') {
+          await item.card.getByRole('button', { name: 'Remove per-note 
process', exact: true }).click();
+        }
+        await expect(item.card.getByRole('button', { name: 'Add per-note 
process', exact: true })).toHaveCount(
+          principal === 'anonymous' ? 0 : 1
+        );
+        const path = 
`/api/interpreter/setting/${encodeURIComponent(setting.name)}`;
+        await interpreterWrites.stub('PUT', path, setting);
+        await item.save.click();
+        await expect(item.edit).toBeVisible();
+        await expect
+          .poll(() => interpreterWrites.requests)
+          .toEqual([
+            {
+              method: 'PUT',
+              path,
+              body: expect.objectContaining({
+                option: expect.objectContaining({
+                  perNote: principal === 'anonymous' ? 'isolated' : 'shared',
+                  perUser: principal === 'anonymous' ? '' : 'isolated',
+                  isUserImpersonate: principal !== 'anonymous'
+                })
+              })
+            }
+          ]);
+      });
+    });
+  }
+
+  test('cancels and confirms restart of a real setting without executing it', 
async ({ page, interpreterWrites }) => {
+    const dialog = page.getByRole('dialog');
+
+    await test.step('When Restart is cancelled, no request is dispatched', 
async () => {
+      await item.restart.click();
+      await expect(dialog).toContainText('Restart Interpreter');
+      await expect(dialog).toContainText('Do you want to restart this 
interpreter?');
+      await dialog.getByRole('button', { name: 'Cancel', exact: true 
}).click();
+      await expect(dialog).toHaveCount(0);
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+
+    await test.step('Then confirmation targets the returned name and shows the 
acknowledgement', async () => {
+      const path = 
`/api/interpreter/setting/restart/${encodeURIComponent(setting.name)}`;
+      await interpreterWrites.stub('PUT', path, null);
+      await item.restart.click();
+      await dialog.getByRole('button', { name: 'OK', exact: true }).click();
+      await expect(
+        page.getByText('Interpreter stopped. Will be lazily started on next 
run.', { exact: true })
+      ).toBeVisible();
+      await 
expect(interpreterPage.settingTitles).toHaveText(serverData.settings.map(interpreterTitlePattern));
+      expect(interpreterWrites.requests).toEqual([{ method: 'PUT', path, body: 
null }]);
+    });
+  });
+
+  test('cancels and confirms removal of the selected real setting', async ({ 
page, interpreterWrites }) => {
+    const dialog = page.getByRole('dialog');
+
+    await test.step('When Remove is cancelled, the setting remains without a 
request', async () => {
+      await item.remove.click();
+      await expect(dialog).toContainText('Remove Interpreter');
+      await expect(dialog).toContainText('Do you want to delete this 
interpreter setting?');
+      await dialog.getByRole('button', { name: 'Cancel', exact: true 
}).click();
+      await expect(dialog).toHaveCount(0);
+      await expect(item.card).toBeVisible();
+      expect(interpreterWrites.requests).toEqual([]);
+    });
+
+    await test.step('Then confirmation removes only that card using an 
intercepted DELETE', async () => {
+      const path = 
`/api/interpreter/setting/${encodeURIComponent(setting.name)}`;
+      await interpreterWrites.stub('DELETE', path, null);
+      await item.remove.click();
+      await dialog.getByRole('button', { name: 'OK', exact: true }).click();
+      await expect(item.card).toHaveCount(0);
+      await 
expect(interpreterPage.settingTitles).toHaveText(serverData.settings.slice(1).map(interpreterTitlePattern));
+      expect(interpreterWrites.requests).toEqual([{ method: 'DELETE', path, 
body: null }]);
+    });
+  });
+});
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html
 
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html
index 607e4987f8..46faac0ee6 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/create-repository-modal/create-repository-modal.component.html
@@ -27,7 +27,12 @@
         <input type="text" nz-input formControlName="url" 
placeholder="Repository url" />
       </nz-input-group>
       <ng-template #addOnBeforeTemplate>
-        <nz-select name="urlProtocol" [(ngModel)]="urlProtocol" 
[ngModelOptions]="{ standalone: true }">
+        <nz-select
+          name="urlProtocol"
+          data-testid="repository-url-protocol"
+          [(ngModel)]="urlProtocol"
+          [ngModelOptions]="{ standalone: true }"
+        >
           <nz-option [nzLabel]="'http://'" [nzValue]="'http://'"></nz-option>
           <nz-option [nzLabel]="'https://'" [nzValue]="'https://'"></nz-option>
           <nz-option [nzLabel]="'file://'" [nzValue]="'file://'"></nz-option>
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.html
 
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.html
index d8b4d39a43..58fce28837 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/interpreter.component.html
@@ -32,11 +32,11 @@
       [nzType]="showRepository ? 'primary' : 'default'"
       (click)="triggerRepository()"
     >
-      <i nz-icon nzType="database" nzTheme="outline"></i>
+      <i nz-icon nzType="database" nzTheme="outline" aria-hidden="true"></i>
       Repository
     </button>
   </ng-template>
-  <div animation-collapse [open]="showRepository">
+  <div animation-collapse [open]="showRepository" 
data-testid="interpreter-repositories">
     <div class="collapse-content">
       <nz-divider nzType="horizontal"></nz-divider>
       <h2>Repositories</h2>
@@ -44,6 +44,7 @@
       @for (repo of repositories; track repo) {
         <nz-tag
           class="repo-item"
+          data-testid="interpreter-repository"
           [nz-tooltip]="repo.url"
           nzTooltipPlacement="topLeft"
           [nzMode]="['central', 'local'].indexOf(repo.id) === -1 ? 'closeable' 
: 'default'"
@@ -52,7 +53,7 @@
           {{ repo.id }}
         </nz-tag>
       }
-      <nz-tag class="editable-tag" (click)="createRepository()">
+      <nz-tag class="editable-tag" data-testid="create-interpreter-repository" 
(click)="createRepository()">
         <i nz-icon nzType="plus"></i>
       </nz-tag>
     </div>
diff --git 
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
 
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
index cb07f5a4b5..01d1e7826f 100644
--- 
a/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
+++ 
b/zeppelin-web-angular/src/app/pages/workspace/interpreter/item/item.component.html
@@ -11,6 +11,7 @@
 -->
 
 <nz-card
+  [attr.data-testid]="mode === 'create' ? 'interpreter-create-form' : 
'interpreter-setting'"
   [class.edit]="mode === 'edit'"
   class="interpreter-item"
   [nzTitle]="mode !== 'create' ? viewModeTitleTemplate : undefined"
@@ -31,7 +32,7 @@
               }
             </small>
           }
-          <span class="interpreter-status">
+          <span class="interpreter-status" data-testid="interpreter-status">
             @switch (interpreter.status) {
               @case ('READY') {
                 <nz-badge nz-tooltip="Ready" nzStatus="success"></nz-badge>
@@ -62,15 +63,15 @@
   <ng-template #extraTemplate>
     <div class="extra-wrap">
       <button nz-button nzSize="small" (click)="switchToEditMode()">
-        <i nz-icon nzType="edit"></i>
+        <i nz-icon nzType="edit" aria-hidden="true"></i>
         Edit
       </button>
       <button nz-button nzSize="small" (click)="handleRestart()">
-        <i nz-icon nzType="reload"></i>
+        <i nz-icon nzType="reload" aria-hidden="true"></i>
         Restart
       </button>
       <button nz-button nzSize="small" (click)="handleRemove()">
-        <i nz-icon nzType="delete"></i>
+        <i nz-icon nzType="delete" aria-hidden="true"></i>
         Remove
       </button>
     </div>
@@ -93,7 +94,7 @@
       <nz-form-item>
         <nz-form-label nzRequired>Interpreter group</nz-form-label>
         <nz-form-control>
-          <nz-select formControlName="group" nzShowSearch>
+          <nz-select formControlName="group" nzShowSearch 
data-testid="interpreter-group">
             @for (item of parent.availableInterpreters; track 
interpretersTrackFn($index, item)) {
               <nz-option [nzValue]="item.name" 
[nzLabel]="item.name"></nz-option>
             }
@@ -249,7 +250,7 @@
       <nz-form-item>
         <nz-form-label>Host</nz-form-label>
         <nz-form-control>
-          <input nz-input formControlName="host" placeholder="" />
+          <input nz-input formControlName="host" placeholder="" 
aria-label="Host" />
         </nz-form-control>
       </nz-form-item>
       <nz-form-item>
@@ -259,6 +260,7 @@
             nz-input
             type="number"
             formControlName="port"
+            aria-label="Port"
             placeholder=""
             
pattern="^()([1-9]|[1-5]?[0-9]{2,4}|6[1-4][0-9]{3}|65[1-4][0-9]{2}|655[1-2][0-9]|6553[1-5])$"
           />
@@ -282,6 +284,7 @@
         >
           <nz-select
             nzMode="tags"
+            data-testid="interpreter-owners"
             class="owners-input"
             [nzTokenSeparators]="[',']"
             style="width: 100%"
@@ -304,7 +307,7 @@
   @if (propertiesFormArray.controls?.length || mode !== 'view') {
     <h3 class="form-title">Properties</h3>
     <form nz-form [formGroup]="formGroup">
-      <nz-table nzTemplateMode nzSize="small">
+      <nz-table nzTemplateMode nzSize="small" 
data-testid="interpreter-properties">
         <thead>
           <tr>
             <th>Name</th>
@@ -451,7 +454,7 @@
   @if (dependenciesFormArray.controls?.length || mode !== 'view') {
     <h3 class="form-title">Dependencies</h3>
     <form nz-form [formGroup]="formGroup">
-      <nz-table nzTemplateMode nzSize="small">
+      <nz-table nzTemplateMode nzSize="small" 
data-testid="interpreter-dependencies">
         <thead>
           <tr>
             <th>Artifact</th>

Reply via email to