From 63e9e0191c89fae701a43a9a04643ccdf3541476 Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Fri, 6 Feb 2026 07:36:18 -0500 Subject: [PATCH 01/12] add UDF test tag --- tests/e2e/tags.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/e2e/tags.ts b/tests/e2e/tags.ts index e0def423c6..dcfb39882e 100644 --- a/tests/e2e/tags.ts +++ b/tests/e2e/tags.ts @@ -27,6 +27,8 @@ export enum Tag { DirectConnectionCRUD = "@direct-connection-crud", /** Tests that upload and delete Flink artifacts. */ FlinkArtifacts = "@flink-artifacts", + /** Tests that create and delete Flink UDFs from artifacts. */ + FlinkUDFs = "@flink-udfs", // Resource-/fixture-specific tags From 046b45b3c1e95e7cb5fb15726335b68247e0e72c Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Fri, 6 Feb 2026 07:37:54 -0500 Subject: [PATCH 02/12] add accessibilityInformation for tree item locator --- src/models/flinkUDF.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/models/flinkUDF.ts b/src/models/flinkUDF.ts index 2407f64684..1927d97315 100644 --- a/src/models/flinkUDF.ts +++ b/src/models/flinkUDF.ts @@ -147,6 +147,7 @@ export class FlinkUdfTreeItem extends TreeItem { this.id = resource.id; this.resource = resource; this.contextValue = `${resource.connectionType.toLowerCase()}-flink-udf`; + this.accessibilityInformation = { label: `Flink UDF: ${resource.name}` }; this.description = `${resource.parametersSignature} → ${formatSqlType(resource.returnType)}`; this.tooltip = createFlinkUdfToolTip(resource); From 27439080dcddafb6db7298be0f4ed333c8e79771 Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Fri, 6 Feb 2026 07:38:14 -0500 Subject: [PATCH 03/12] add locator getters for UDF container + tree items --- tests/e2e/objects/views/FlinkDatabaseView.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/e2e/objects/views/FlinkDatabaseView.ts b/tests/e2e/objects/views/FlinkDatabaseView.ts index 011de0daa8..e9f6d4c517 100644 --- a/tests/e2e/objects/views/FlinkDatabaseView.ts +++ b/tests/e2e/objects/views/FlinkDatabaseView.ts @@ -41,6 +41,16 @@ export class FlinkDatabaseView extends SearchableView { return this.treeItems.and(this.page.locator('[aria-label^="Flink Artifact: "]')); } + /** Get the UDFs container item. */ + get udfsContainer(): Locator { + return this.treeItems.filter({ hasText: "UDFs" }).first(); + } + + /** UDF items based on their accessibilityInformation label. */ + get udfs(): Locator { + return this.treeItems.and(this.page.locator('[aria-label^="Flink UDF: "]')); + } + /** * Load the Flink Artifacts view by selecting a Kafka cluster as the Flink database, * using the specified entrypoint. From 3713f01987dfc6f6293ffafe33ff43430aa98c4d Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Mon, 4 May 2026 11:09:17 -0400 Subject: [PATCH 04/12] update tag comment for accuracy --- tests/e2e/tags.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/e2e/tags.ts b/tests/e2e/tags.ts index dcfb39882e..ea727195ca 100644 --- a/tests/e2e/tags.ts +++ b/tests/e2e/tags.ts @@ -27,7 +27,7 @@ export enum Tag { DirectConnectionCRUD = "@direct-connection-crud", /** Tests that upload and delete Flink artifacts. */ FlinkArtifacts = "@flink-artifacts", - /** Tests that create and delete Flink UDFs from artifacts. */ + /** Tests that create and delete Flink UDFs. */ FlinkUDFs = "@flink-udfs", // Resource-/fixture-specific tags From 9dee0e797f517bd6447659b127a5107f3bcecdd6 Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 09:31:47 -0400 Subject: [PATCH 05/12] set up `artifact` fixture --- tests/e2e/baseTest.ts | 58 +++++++++++++++++++++++++++++++++++++ tests/e2e/types/artifact.ts | 13 +++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/e2e/types/artifact.ts diff --git a/tests/e2e/baseTest.ts b/tests/e2e/baseTest.ts index c07adbd47e..8b12b53ae3 100644 --- a/tests/e2e/baseTest.ts +++ b/tests/e2e/baseTest.ts @@ -5,10 +5,12 @@ import { stubAllDialogs, stubDialog } from "electron-playwright-helpers"; import { createWriteStream, existsSync, mkdtempSync, readFileSync } from "fs"; import { tmpdir } from "os"; import path from "path"; +import { fileURLToPath } from "url"; import { DEBUG_LOGGING_ENABLED } from "./constants"; import { Notification } from "./objects/notifications/Notification"; import { NotificationArea } from "./objects/notifications/NotificationArea"; import { Quickpick } from "./objects/quickInputs/Quickpick"; +import { FlinkDatabaseView, SelectFlinkDatabase } from "./objects/views/FlinkDatabaseView"; import { DEFAULT_CCLOUD_TOPIC_REPLICATION_FACTOR, SelectKafkaCluster, @@ -17,6 +19,7 @@ import { import type { CCloudConnectionItem } from "./objects/views/viewItems/CCloudConnectionItem"; import type { DirectConnectionItem } from "./objects/views/viewItems/DirectConnectionItem"; import type { LocalConnectionItem } from "./objects/views/viewItems/LocalConnectionItem"; +import type { ArtifactConfig } from "./types/artifact"; import type { DirectConnectionOptions, LocalConnectionOptions } from "./types/connection"; import { ConnectionType, FormConnectionType, SupportedAuthType } from "./types/connection"; import type { TopicConfig } from "./types/topic"; @@ -98,6 +101,16 @@ interface VSCodeFixtures { * `name` for tests to reference. */ topic: string; + + /** + * Configuration options for creating an artifact with the {@linkcode artifact} fixture. + */ + artifactConfig: ArtifactConfig | undefined; + /** + * Set up a Flink artifact based on the {@linkcode artifactConfig} option and return the + * artifact `name` for tests to reference. Tears down the artifact after the test completes. + */ + artifact: string; } export const test = testBase.extend({ @@ -352,6 +365,51 @@ export const test = testBase.extend({ await topicsView.loadTopics(connectionType, SelectKafkaCluster.FromResourcesView, clusterLabel); await topicsView.deleteTopic(topicName); }, + + // no default value, must be provided by test + artifactConfig: [undefined, { option: true }], + + artifact: async ({ electronApp, page, connectionItem, artifactConfig }, use) => { + if (!artifactConfig) { + throw new Error("artifactConfig must be set, like `test.use({ artifactConfig: {} })`"); + } + + await expect(connectionItem.locator).toHaveAttribute("aria-expanded", "true"); + + const entrypoint = artifactConfig.entrypoint ?? SelectFlinkDatabase.FromDatabaseViewButton; + const jarPath = + artifactConfig.jarPath ?? + path.join( + path.dirname(fileURLToPath(import.meta.url)), + "..", + "fixtures", + "flink-artifacts", + "udfs-simple.jar", + ); + + const artifactsView = new FlinkDatabaseView(page); + + if (artifactConfig.provider && artifactConfig.region) { + await artifactsView.selectKafkaClusterByProviderRegion( + artifactConfig.provider, + artifactConfig.region, + ); + } else { + await artifactsView.ensureExpanded(); + await artifactsView.loadArtifacts(entrypoint); + } + + const artifactName = await artifactsView.uploadFlinkArtifact(electronApp, jarPath); + await artifactsView.waitForUploadSuccess(); + + await use(artifactName); + + await openConfluentSidebar(page); + await artifactsView.ensureExpanded(); + // deleteFlinkArtifact searches to isolate the artifact, so no need to manually expand + // the Artifacts container here (and a prior search from the test body could hide it) + await artifactsView.deleteFlinkArtifact(artifactName); + }, }); /** diff --git a/tests/e2e/types/artifact.ts b/tests/e2e/types/artifact.ts new file mode 100644 index 0000000000..990dc25e6c --- /dev/null +++ b/tests/e2e/types/artifact.ts @@ -0,0 +1,13 @@ +import type { SelectFlinkDatabase } from "../objects/views/FlinkDatabaseView"; + +/** Configuration for creating a Flink artifact. */ +export interface ArtifactConfig { + /** Path to the JAR file. Defaults to the `udfs-simple.jar` fixture. */ + jarPath?: string; + /** Entrypoint for uploading. Defaults to FromDatabaseViewButton. */ + entrypoint?: SelectFlinkDatabase; + /** Cloud provider. Defaults to "AWS". */ + provider?: string; + /** Cloud region. Defaults to "us-east-2". */ + region?: string; +} From aa3da79c399e1c8de520a9411694cf400d3257d9 Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 09:35:44 -0400 Subject: [PATCH 06/12] comment cleanup --- tests/e2e/baseTest.ts | 3 +-- tests/e2e/types/artifact.ts | 6 +++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/tests/e2e/baseTest.ts b/tests/e2e/baseTest.ts index 8b12b53ae3..e3457edb71 100644 --- a/tests/e2e/baseTest.ts +++ b/tests/e2e/baseTest.ts @@ -406,8 +406,7 @@ export const test = testBase.extend({ await openConfluentSidebar(page); await artifactsView.ensureExpanded(); - // deleteFlinkArtifact searches to isolate the artifact, so no need to manually expand - // the Artifacts container here (and a prior search from the test body could hide it) + // deleteFlinkArtifact searches internally; expanding here could be hidden by a prior search await artifactsView.deleteFlinkArtifact(artifactName); }, }); diff --git a/tests/e2e/types/artifact.ts b/tests/e2e/types/artifact.ts index 990dc25e6c..0d8808b33a 100644 --- a/tests/e2e/types/artifact.ts +++ b/tests/e2e/types/artifact.ts @@ -4,10 +4,10 @@ import type { SelectFlinkDatabase } from "../objects/views/FlinkDatabaseView"; export interface ArtifactConfig { /** Path to the JAR file. Defaults to the `udfs-simple.jar` fixture. */ jarPath?: string; - /** Entrypoint for uploading. Defaults to FromDatabaseViewButton. */ + /** Entrypoint for uploading. Defaults to {@linkcode SelectFlinkDatabase.FromDatabaseViewButton}. */ entrypoint?: SelectFlinkDatabase; - /** Cloud provider. Defaults to "AWS". */ + /** Cloud provider; if both `provider` and `region` are set, the upload skips the entrypoint flow. */ provider?: string; - /** Cloud region. Defaults to "us-east-2". */ + /** Cloud region; if both `provider` and `region` are set, the upload skips the entrypoint flow. */ region?: string; } From 8d2f79c27b944316ced66bcb997b8bb152033449 Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 09:37:48 -0400 Subject: [PATCH 07/12] update FlinkDatabaseView model locator; add helper methods for UDF registration/deletion --- tests/e2e/objects/views/FlinkDatabaseView.ts | 62 ++++++++++++++++++-- 1 file changed, 58 insertions(+), 4 deletions(-) diff --git a/tests/e2e/objects/views/FlinkDatabaseView.ts b/tests/e2e/objects/views/FlinkDatabaseView.ts index e9f6d4c517..7bf04120c8 100644 --- a/tests/e2e/objects/views/FlinkDatabaseView.ts +++ b/tests/e2e/objects/views/FlinkDatabaseView.ts @@ -43,7 +43,7 @@ export class FlinkDatabaseView extends SearchableView { /** Get the UDFs container item. */ get udfsContainer(): Locator { - return this.treeItems.filter({ hasText: "UDFs" }).first(); + return this.treeItems.filter({ hasText: /^UDFs/ }).first(); } /** UDF items based on their accessibilityInformation label. */ @@ -214,6 +214,61 @@ export class FlinkDatabaseView extends SearchableView { await this.expandContainer(this.artifactsContainer); } + /** Expand the UDFs container to show any available UDF tree items. */ + async expandUdfsContainer(): Promise { + await this.expandContainer(this.udfsContainer); + } + + /** + * Run the "Register As UDF" guided flow for the given artifact, providing `className` and + * `functionName` to the series of quickinput boxes. + */ + async startGuidedUdfCreation( + artifactName: string, + className: string, + functionName: string, + ): Promise { + await this.expandArtifactsContainer(); + const artifactLocator = this.artifacts.filter({ hasText: artifactName }); + await expect(artifactLocator).toHaveCount(1); + const artifactItem = new ViewItem(this.page, artifactLocator.first()); + await artifactItem.rightClickContextMenuAction("Register As UDF"); + + const classNameInput = new InputBox(this.page); + await expect(classNameInput.locator).toBeVisible(); + await classNameInput.input.fill(className); + await classNameInput.confirm(); + + const functionNameInput = new InputBox(this.page); + await expect(functionNameInput.locator).toBeVisible(); + await functionNameInput.input.fill(functionName); + await functionNameInput.confirm(); + } + + /** Open the "Register With Flink SQL" document for the given artifact. */ + async openUdfRegistrationDocument(artifactName: string): Promise { + await this.expandArtifactsContainer(); + const artifactLocator = this.artifacts.filter({ hasText: artifactName }); + await expect(artifactLocator).toHaveCount(1); + const artifactItem = new ViewItem(this.page, artifactLocator.first()); + await artifactItem.rightClickContextMenuAction("Register With Flink SQL"); + } + + /** Delete a Flink UDF via its tree-item context menu. */ + async deleteFlinkUdf(functionName: string): Promise { + const udfLocator = await this.getItemByLabel(functionName); + const udfItem = new ViewItem(this.page, udfLocator); + await udfItem.locator.scrollIntoViewIfNeeded(); + await expect(udfItem.locator).toBeVisible(); + + await udfItem.rightClickContextMenuAction("Delete UDF"); + const notificationArea = new NotificationArea(this.page); + const successNotifications = notificationArea.infoNotifications.filter({ + hasText: "deleted successfully", + }); + await expect(successNotifications).not.toHaveCount(0); + } + /** * Click the upload button on the Artifacts container to initiate the artifact upload flow. */ @@ -294,9 +349,8 @@ export class FlinkDatabaseView extends SearchableView { * @param artifactName - The name of the artifact to delete */ async deleteFlinkArtifact(artifactName: string): Promise { - const artifactLocator = this.artifacts.filter({ hasText: artifactName }); - await expect(artifactLocator).toHaveCount(1); - const artifactItem = new ViewItem(this.page, artifactLocator.first()); + const artifactLocator = await this.getItemByLabel(artifactName); + const artifactItem = new ViewItem(this.page, artifactLocator); await artifactItem.locator.scrollIntoViewIfNeeded(); await expect(artifactItem.locator).toBeVisible(); From 5c3cbafdcba5dfb00ea8f92906c9ac3a26c70e7c Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 09:44:50 -0400 Subject: [PATCH 08/12] add Flink UDF E2E tests --- tests/e2e/specs/flinkUdf.spec.ts | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) create mode 100644 tests/e2e/specs/flinkUdf.spec.ts diff --git a/tests/e2e/specs/flinkUdf.spec.ts b/tests/e2e/specs/flinkUdf.spec.ts new file mode 100644 index 0000000000..357a2865cd --- /dev/null +++ b/tests/e2e/specs/flinkUdf.spec.ts @@ -0,0 +1,88 @@ +import { expect } from "@playwright/test"; +import { test } from "../baseTest"; +import { NotificationArea } from "../objects/notifications/NotificationArea"; +import { FlinkDatabaseView } from "../objects/views/FlinkDatabaseView"; +import { Tag } from "../tags"; +import { ConnectionType } from "../types/connection"; +import { randomHexString } from "../utils/strings"; + +test.describe("Flink UDFs", { tag: [Tag.CCloud, Tag.FlinkUDFs] }, () => { + test.use({ connectionType: ConnectionType.Ccloud }); + + // TODO: add GCP, see https://github.com/confluentinc/vscode/issues/2817 + const providers = [ + { provider: "AWS", region: "us-east-2" }, + { provider: "AZURE", region: "eastus" }, + ]; + + for (const { provider, region } of providers) { + test.describe(provider, () => { + test.use({ artifactConfig: { provider, region } }); + + test("should create a UDF via the guided flow", async ({ page, artifact }) => { + const flinkDatabaseView = new FlinkDatabaseView(page); + const functionName = `test_udf_${randomHexString(6)}`; + + await flinkDatabaseView.startGuidedUdfCreation( + artifact, + "io.confluent.udf.examples.scalar.SumScalarFunction", + functionName, + ); + + const notificationArea = new NotificationArea(page); + const successNotifications = notificationArea.infoNotifications.filter({ + hasText: "function created successfully", + }); + await expect(successNotifications.first()).toBeVisible({ timeout: 60_000 }); + + // search auto-expands the matching container, so we don't need to expand UDFs manually + const udfItem = await flinkDatabaseView.getItemByLabel(functionName); + await expect(udfItem).toBeVisible(); + }); + + test("should open a Flink SQL document with the appropriate content", async ({ + page, + artifact, + }) => { + const flinkDatabaseView = new FlinkDatabaseView(page); + + await flinkDatabaseView.openUdfRegistrationDocument(artifact); + + const editorContent = page.locator(".monaco-editor .view-lines"); + await expect(editorContent).toBeVisible(); + + const editorText = await editorContent.textContent(); + expect(editorText).toMatch(/CREATE\s+FUNCTION/); + expect(editorText).toMatch(/USING\s+JAR/); + expect(editorText).toContain("confluent-artifact://"); + // not testing the statement submission behavior here since those are covered by the + // @flink-statements tests, and the document is just a template with snippet placeholders + }); + + test("should delete a UDF", async ({ page, artifact }) => { + const flinkDatabaseView = new FlinkDatabaseView(page); + const functionName = `test_udf_delete_${randomHexString(6)}`; + + await flinkDatabaseView.startGuidedUdfCreation( + artifact, + "io.confluent.udf.examples.scalar.ConcatScalarFunction", + functionName, + ); + + const notificationArea = new NotificationArea(page); + const createNotifications = notificationArea.infoNotifications.filter({ + hasText: "function created successfully", + }); + await expect(createNotifications.first()).toBeVisible({ timeout: 60_000 }); + + const udfItemBefore = await flinkDatabaseView.getItemByLabel(functionName); + await expect(udfItemBefore).toBeVisible(); + + await flinkDatabaseView.deleteFlinkUdf(functionName); + // search filter from deleteFlinkUdf is still applied, so the deleted UDF drops out + const udfItemAfter = flinkDatabaseView.udfs.filter({ hasText: functionName }); + await expect(udfItemAfter).toHaveCount(0); + }); + }); + } +}); From cd954af3e874e8f328257ed61796578e25cb88ce Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 09:45:00 -0400 Subject: [PATCH 09/12] update default CUJ params Co-authored-by: Copilot --- .semaphore/semaphore.yml | 2 +- service.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.semaphore/semaphore.yml b/.semaphore/semaphore.yml index 177fd666fb..ba84773f1f 100644 --- a/.semaphore/semaphore.yml +++ b/.semaphore/semaphore.yml @@ -411,7 +411,7 @@ promotions: parameters: env_vars: - name: TEST_SUITE - default_value: "@smoke|@topic-message-viewer|@evolve-schema|@produce-message-to-topic|@project-scaffolding|@flink-statements|@direct-connection-crud|@flink-artifacts" + default_value: "@smoke|@topic-message-viewer|@evolve-schema|@produce-message-to-topic|@project-scaffolding|@flink-statements|@direct-connection-crud|@flink-artifacts|@flink-udfs" description: "Playwright E2E test tags to run, separated by |. By default, runs the smoketests and CUJ-related tests." - name: VSCODE_VERSION default_value: "stable" diff --git a/service.yml b/service.yml index 3a9af79558..b45ff506f0 100644 --- a/service.yml +++ b/service.yml @@ -54,7 +54,7 @@ semaphore: - name: TEST_SUITE required: false description: The test name or tag(s) (pipe-separated, e.g. `@ccloud|@direct`) to run. If not specified, will run all E2E tests. - default_value: "@smoke|@topic-message-viewer|@evolve-schema|@produce-message-to-topic|@project-scaffolding|@flink-statements|@direct-connection-crud|@flink-artifacts" + default_value: "@smoke|@topic-message-viewer|@evolve-schema|@produce-message-to-topic|@project-scaffolding|@flink-statements|@direct-connection-crud|@flink-artifacts|@flink-udfs" - name: PLATFORM required: true options: @@ -119,7 +119,7 @@ semaphore: - name: TEST_SUITE required: true description: The test tag(s) (pipe-separated, e.g. `@smoke|@topic-message-viewer`) to run. By default, runs the smoke tests and critical user flow tests. - default_value: "@smoke|@topic-message-viewer|@evolve-schema|@produce-message-to-topic|@project-scaffolding|@flink-statements|@direct-connection-crud|@flink-artifacts" + default_value: "@smoke|@topic-message-viewer|@evolve-schema|@produce-message-to-topic|@project-scaffolding|@flink-statements|@direct-connection-crud|@flink-artifacts|@flink-udfs" - name: PLATFORM required: true options: From 8c27ff034bf58b8e9f9072ba5ffb03b5acbd525f Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 11:39:57 -0400 Subject: [PATCH 10/12] clean up artifact fixture... cleanup --- tests/e2e/baseTest.ts | 24 +++++++++++------- tests/e2e/specs/flinkArtifact.spec.ts | 36 +++++++++++++++++++++------ 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/tests/e2e/baseTest.ts b/tests/e2e/baseTest.ts index e3457edb71..a4e08605dc 100644 --- a/tests/e2e/baseTest.ts +++ b/tests/e2e/baseTest.ts @@ -399,15 +399,21 @@ export const test = testBase.extend({ await artifactsView.loadArtifacts(entrypoint); } - const artifactName = await artifactsView.uploadFlinkArtifact(electronApp, jarPath); - await artifactsView.waitForUploadSuccess(); - - await use(artifactName); - - await openConfluentSidebar(page); - await artifactsView.ensureExpanded(); - // deleteFlinkArtifact searches internally; expanding here could be hidden by a prior search - await artifactsView.deleteFlinkArtifact(artifactName); + // track the artifact name across try/finally so cleanup runs even if a + // post-upload setup step throws before use() + let artifactName: string | undefined; + try { + artifactName = await artifactsView.uploadFlinkArtifact(electronApp, jarPath); + await artifactsView.waitForUploadSuccess(); + await use(artifactName); + } finally { + if (artifactName) { + await openConfluentSidebar(page); + await artifactsView.ensureExpanded(); + // deleteFlinkArtifact searches internally; expanding here could be hidden by a prior search + await artifactsView.deleteFlinkArtifact(artifactName); + } + } }, }); diff --git a/tests/e2e/specs/flinkArtifact.spec.ts b/tests/e2e/specs/flinkArtifact.spec.ts index 259b1f48ed..de8d58acb1 100644 --- a/tests/e2e/specs/flinkArtifact.spec.ts +++ b/tests/e2e/specs/flinkArtifact.spec.ts @@ -26,7 +26,23 @@ test.describe("Flink Artifacts", { tag: [Tag.CCloud, Tag.FlinkArtifacts] }, () = await expect(connectionItem.locator).toHaveAttribute("aria-expanded", "true"); }); - test.afterEach(async () => { + test.afterEach(async ({ page }) => { + // server-side cleanup: if the happy-path test didn't reach its inline delete + // (e.g. an earlier assertion failed), drop the orphaned artifact here so + // reruns don't pile up dangling artifacts on the cluster. Best-effort: a + // failed cleanup must not mask the original test failure, hence the catch. + if (uploadedArtifactName) { + try { + const artifactsView = new FlinkDatabaseView(page); + await openConfluentSidebar(page); + await artifactsView.ensureExpanded(); + await artifactsView.deleteFlinkArtifact(uploadedArtifactName); + } catch (err) { + console.warn(`failed to clean up orphaned artifact "${uploadedArtifactName}":`, err); + } + uploadedArtifactName = undefined; + } + // Only delete temporary test files, not permanent fixtures const permanentFixture = path.join(fixturesDir, "udfs-simple.jar"); if (artifactPath && existsSync(artifactPath) && artifactPath !== permanentFixture) { @@ -36,6 +52,8 @@ test.describe("Flink Artifacts", { tag: [Tag.CCloud, Tag.FlinkArtifacts] }, () = /** The path to an artifact created by the test suite, to be cleaned up in afterEach. */ let artifactPath: string | undefined; + /** The name of an artifact uploaded to CCloud, tracked so afterEach can clean it up if a happy-path test fails before its inline deletion runs. */ + let uploadedArtifactName: string | undefined; const fixturesDir = path.join(__dirname, "..", "..", "fixtures", "flink-artifacts"); @@ -83,7 +101,7 @@ test.describe("Flink Artifacts", { tag: [Tag.CCloud, Tag.FlinkArtifacts] }, () = await artifactsView.ensureExpanded(); await artifactsView.loadArtifacts(entrypoint); - const uploadedArtifactName = await startUploadFlow( + const artifactName = await startUploadFlow( entrypoint, page, electronApp, @@ -92,6 +110,10 @@ test.describe("Flink Artifacts", { tag: [Tag.CCloud, Tag.FlinkArtifacts] }, () = region, artifactPath, ); + // record the upload so afterEach can clean it up if the rest of the + // test throws before the inline deletion below + uploadedArtifactName = artifactName; + await artifactsView.waitForUploadSuccess(); const notificationArea = new NotificationArea(page); const successNotifications = notificationArea.infoNotifications.filter({ @@ -99,15 +121,15 @@ test.describe("Flink Artifacts", { tag: [Tag.CCloud, Tag.FlinkArtifacts] }, () = }); await expect(successNotifications.first()).toBeVisible(); const artifactViewItem = await artifactsView.getDatabaseResourceByLabel( - uploadedArtifactName, + artifactName, artifactsView.artifactsContainer, ); await expect(artifactViewItem).toBeVisible(); - await artifactsView.deleteFlinkArtifact(uploadedArtifactName); - await expect(artifactsView.artifacts.filter({ hasText: uploadedArtifactName })).toHaveCount( - 0, - ); + await artifactsView.deleteFlinkArtifact(artifactName); + await expect(artifactsView.artifacts.filter({ hasText: artifactName })).toHaveCount(0); + // inline deletion succeeded, so afterEach has no server-side work to do + uploadedArtifactName = undefined; }); test(`should fail to upload a jar exceeding the file limit [${provider}/${region}] - ${testName}`, async ({ From f821ced9abe464fab2ba330888b6a19b2d1824dd Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 11:40:07 -0400 Subject: [PATCH 11/12] use TextDocument model --- tests/e2e/specs/flinkUdf.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/e2e/specs/flinkUdf.spec.ts b/tests/e2e/specs/flinkUdf.spec.ts index 357a2865cd..a633ff5ca1 100644 --- a/tests/e2e/specs/flinkUdf.spec.ts +++ b/tests/e2e/specs/flinkUdf.spec.ts @@ -1,5 +1,6 @@ import { expect } from "@playwright/test"; import { test } from "../baseTest"; +import { TextDocument } from "../objects/editor/TextDocument"; import { NotificationArea } from "../objects/notifications/NotificationArea"; import { FlinkDatabaseView } from "../objects/views/FlinkDatabaseView"; import { Tag } from "../tags"; @@ -48,13 +49,12 @@ test.describe("Flink UDFs", { tag: [Tag.CCloud, Tag.FlinkUDFs] }, () => { await flinkDatabaseView.openUdfRegistrationDocument(artifact); - const editorContent = page.locator(".monaco-editor .view-lines"); - await expect(editorContent).toBeVisible(); - - const editorText = await editorContent.textContent(); - expect(editorText).toMatch(/CREATE\s+FUNCTION/); - expect(editorText).toMatch(/USING\s+JAR/); - expect(editorText).toContain("confluent-artifact://"); + // the registration command opens a fresh untitled doc with snippets + const registrationDoc = new TextDocument(page, "Untitled-1"); + await expect(registrationDoc.locator).toBeVisible(); + await expect(registrationDoc.editorContent).toContainText(/CREATE\s+FUNCTION/); + await expect(registrationDoc.editorContent).toContainText(/USING\s+JAR/); + await expect(registrationDoc.editorContent).toContainText("confluent-artifact://"); // not testing the statement submission behavior here since those are covered by the // @flink-statements tests, and the document is just a template with snippet placeholders }); From fd942d23ddd86d10767c884354fc4f9ed795fdfe Mon Sep 17 00:00:00 2001 From: Dave Shoup Date: Tue, 5 May 2026 11:41:05 -0400 Subject: [PATCH 12/12] pass artifact/udf locators to getItemByLabel call points --- tests/e2e/objects/views/FlinkDatabaseView.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/e2e/objects/views/FlinkDatabaseView.ts b/tests/e2e/objects/views/FlinkDatabaseView.ts index 7bf04120c8..d0f934d955 100644 --- a/tests/e2e/objects/views/FlinkDatabaseView.ts +++ b/tests/e2e/objects/views/FlinkDatabaseView.ts @@ -256,7 +256,9 @@ export class FlinkDatabaseView extends SearchableView { /** Delete a Flink UDF via its tree-item context menu. */ async deleteFlinkUdf(functionName: string): Promise { - const udfLocator = await this.getItemByLabel(functionName); + // scope to UDF tree items so the search result can't match a same-named item from a + // different container (e.g. an artifact reusing the suffix) + const udfLocator = await this.getItemByLabel(functionName, this.udfs); const udfItem = new ViewItem(this.page, udfLocator); await udfItem.locator.scrollIntoViewIfNeeded(); await expect(udfItem.locator).toBeVisible(); @@ -349,7 +351,9 @@ export class FlinkDatabaseView extends SearchableView { * @param artifactName - The name of the artifact to delete */ async deleteFlinkArtifact(artifactName: string): Promise { - const artifactLocator = await this.getItemByLabel(artifactName); + // scope to artifact tree items so the search result can't match a same-named item from a + // different container (e.g. a UDF reusing the suffix) + const artifactLocator = await this.getItemByLabel(artifactName, this.artifacts); const artifactItem = new ViewItem(this.page, artifactLocator); await artifactItem.locator.scrollIntoViewIfNeeded(); await expect(artifactItem.locator).toBeVisible();