From 8ca3f6a3f43cdd6ed9c091460f8b06f857dcb24e Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:21:31 +0100 Subject: [PATCH 01/21] tests: init --- .github/workflows/ci.yml | 32 +++++ package.json | 4 +- src/test/extension.test.ts | 52 +++++++- src/test/fixtures/testChart/.helmignore | 23 ++++ src/test/fixtures/testChart/Chart.yaml | 24 ++++ .../fixtures/testChart/templates/NOTES.txt | 22 ++++ .../fixtures/testChart/templates/_helpers.tpl | 62 +++++++++ .../testChart/templates/deployment.yaml | 78 +++++++++++ src/test/fixtures/testChart/values.yaml | 123 ++++++++++++++++++ tsconfig.json | 9 +- 10 files changed, 421 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 src/test/fixtures/testChart/.helmignore create mode 100644 src/test/fixtures/testChart/Chart.yaml create mode 100644 src/test/fixtures/testChart/templates/NOTES.txt create mode 100644 src/test/fixtures/testChart/templates/_helpers.tpl create mode 100644 src/test/fixtures/testChart/templates/deployment.yaml create mode 100644 src/test/fixtures/testChart/values.yaml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..848b61a --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,32 @@ +name: 'CI' + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22.x + - name: Install dependencies + run: npm install + - name: Run tests on Linux + if: runner.os == 'Linux' + uses: coactions/setup-xvfb@v1 + with: + run: npm test + + - name: Run tests on Windows and macOS + if: runner.os != 'Linux' + run: npm test diff --git a/package.json b/package.json index 96ed5b4..16c3352 100644 --- a/package.json +++ b/package.json @@ -89,8 +89,8 @@ "package": "webpack --mode production --devtool hidden-source-map", "compile-tests": "tsc -p . --outDir out", "watch-tests": "tsc -p . -w --outDir out", - "pretest": "npm run compile-tests && npm run compile && npm run lint", - "lint": "eslint src --ext ts", + "pretest": "npm run compile-tests && npm run compile", + "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext ts", "test": "vscode-test", "release": "release-it" }, diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 2f671d3..57e3b50 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,4 +1,5 @@ import * as assert from "assert"; +import * as path from "path"; // You can import and use all API from the 'vscode' module // as well as import your extension to test it @@ -6,10 +7,51 @@ import * as vscode from "vscode"; // import * as myExtension from '../../extension'; suite("Extension Test Suite", () => { - vscode.window.showInformationMessage("Start all tests."); + vscode.window.showInformationMessage('Start all tests.'); - test("Sample test", () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); + test('Sample test', () => { + assert.strictEqual(-1, [1, 2, 3].indexOf(5)); + assert.strictEqual(-1, [1, 2, 3].indexOf(0)); + }); + + test('Hover tests', async () => { + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, 'No workspace folder found'); + const docPath = path.join(workspaceFolder.uri.fsPath, 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml'); + const docUri = vscode.Uri.file(docPath); + const document = await vscode.workspace.openTextDocument(docUri); + await vscode.window.showTextDocument(document); + + // Test hover on a Helm property: .Values.replicaCount at line 9 + const helmPosition = new vscode.Position(8, 25); // Position inside 'replicaCount' + const helmHovers = await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + docUri, + helmPosition + ); + + assert.strictEqual(helmHovers.length, 1, 'Expected one hover for Helm property'); + const helmHoverContent = helmHovers[0].contents[0] as vscode.MarkdownString; + // Assuming replicaCount is 1 in values.yaml + assert.ok(helmHoverContent.value.includes('`1`'), 'Hover content for Helm property should show the value.'); + assert.ok( + helmHoverContent.value.includes('values.yaml'), + 'Hover content for Helm property should mention the source file.' + ); + + // Test hover on a YAML property: spec at line 7 + const yamlPosition = new vscode.Position(6, 3); // Position inside 'spec' + const yamlHovers = await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + docUri, + yamlPosition + ); + + assert.strictEqual(yamlHovers.length, 1, 'Expected one hover for YAML property'); + const yamlHoverContent = yamlHovers[0].contents[0] as vscode.MarkdownString; + assert.ok( + yamlHoverContent.value.includes('Specification of the desired behavior of the Deployment.'), + 'Hover content for YAML property should show documentation.' + ); + }); }); diff --git a/src/test/fixtures/testChart/.helmignore b/src/test/fixtures/testChart/.helmignore new file mode 100644 index 0000000..0e8a0eb --- /dev/null +++ b/src/test/fixtures/testChart/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/src/test/fixtures/testChart/Chart.yaml b/src/test/fixtures/testChart/Chart.yaml new file mode 100644 index 0000000..cd8a397 --- /dev/null +++ b/src/test/fixtures/testChart/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: testChart +description: A Helm chart for Kubernetes + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "1.16.0" diff --git a/src/test/fixtures/testChart/templates/NOTES.txt b/src/test/fixtures/testChart/templates/NOTES.txt new file mode 100644 index 0000000..dfbaf1e --- /dev/null +++ b/src/test/fixtures/testChart/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "testChart.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "testChart.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "testChart.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "testChart.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/src/test/fixtures/testChart/templates/_helpers.tpl b/src/test/fixtures/testChart/templates/_helpers.tpl new file mode 100644 index 0000000..dfe738e --- /dev/null +++ b/src/test/fixtures/testChart/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "testChart.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "testChart.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "testChart.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "testChart.labels" -}} +helm.sh/chart: {{ include "testChart.chart" . }} +{{ include "testChart.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "testChart.selectorLabels" -}} +app.kubernetes.io/name: {{ include "testChart.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "testChart.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "testChart.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/src/test/fixtures/testChart/templates/deployment.yaml b/src/test/fixtures/testChart/templates/deployment.yaml new file mode 100644 index 0000000..c5e21e9 --- /dev/null +++ b/src/test/fixtures/testChart/templates/deployment.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "testChart.fullname" . }} + labels: + {{- include "testChart.labels" . | nindent 4 }} +spec: + {{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} + {{- end }} + selector: + matchLabels: + {{- include "testChart.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "testChart.labels" . | nindent 8 }} + {{- with .Values.podLabels }} + {{- toYaml . | nindent 8 }} + {{- end }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "testChart.serviceAccountName" . }} + {{- with .Values.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: {{ .Chart.Name }} + {{- with .Values.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.service.port }} + protocol: TCP + {{- with .Values.livenessProbe }} + livenessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.readinessProbe }} + readinessProbe: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.resources }} + resources: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} + {{- with .Values.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/src/test/fixtures/testChart/values.yaml b/src/test/fixtures/testChart/values.yaml new file mode 100644 index 0000000..51ef389 --- /dev/null +++ b/src/test/fixtures/testChart/values.yaml @@ -0,0 +1,123 @@ +# Default values for testChart. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +# This will set the replicaset count more information can be found here: https://kubernetes.io/docs/concepts/workloads/controllers/replicaset/ +replicaCount: 1 + +# This sets the container image more information can be found here: https://kubernetes.io/docs/concepts/containers/images/ +image: + repository: nginx + # This sets the pull policy for images. + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + +# This is for the secrets for pulling an image from a private repository more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/pull-image-private-registry/ +imagePullSecrets: [] +# This is to override the chart name. +nameOverride: "" +fullnameOverride: "" + +# This section builds out the service account more information can be found here: https://kubernetes.io/docs/concepts/security/service-accounts/ +serviceAccount: + # Specifies whether a service account should be created + create: true + # Automatically mount a ServiceAccount's API credentials? + automount: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +# This is for setting Kubernetes Annotations to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/ +podAnnotations: {} +# This is for setting Kubernetes Labels to a Pod. +# For more information checkout: https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/ +podLabels: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +# This is for setting up a service more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/ +service: + # This sets the service type more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + type: ClusterIP + # This sets the ports more information can be found here: https://kubernetes.io/docs/concepts/services-networking/service/#field-spec-ports + port: 80 + +# This block is for setting up the ingress for more information can be found here: https://kubernetes.io/docs/concepts/services-networking/ingress/ +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +# This is to setup the liveness and readiness probes more information can be found here: https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/ +livenessProbe: + httpGet: + path: / + port: http +readinessProbe: + httpGet: + path: / + port: http + +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +# Additional volumes on the output Deployment definition. +volumes: [] +# - name: foo +# secret: +# secretName: mysecret +# optional: false + +# Additional volumeMounts on the output Deployment definition. +volumeMounts: [] +# - name: foo +# mountPath: "/etc/foo" +# readOnly: true + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/tsconfig.json b/tsconfig.json index 8a79f20..101bc85 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,7 @@ { "compilerOptions": { "module": "Node16", + "moduleResolution": "node16", "target": "ES2022", "lib": [ "ES2022" @@ -12,5 +13,11 @@ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */ - } + }, + "exclude": [ + "node_modules", + ".vscode-test", + "dist", + "out" + ] } From b11a901a8d3a026ac4f4fc5d3b5aa4430cb08752 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:23:55 +0100 Subject: [PATCH 02/21] fix --- src/test/executable.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/executable.test.ts b/src/test/executable.test.ts index 747b4a9..f569cd7 100644 --- a/src/test/executable.test.ts +++ b/src/test/executable.test.ts @@ -69,6 +69,7 @@ class MockExtensionContext implements vscode.ExtensionContext { delete: () => Promise.resolve(), onDidChange: new vscode.EventEmitter() .event, + keys: () => Promise.resolve([]), }; asAbsolutePath = (relativePath: string) => path.join(this.extensionPath, relativePath); From 1e87371e1db2525caad997a6fab4ea314e21c77e Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:26:58 +0100 Subject: [PATCH 03/21] fix: --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 16c3352..7877a42 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext ts", - "test": "vscode-test", + "test": "vscode-test .", "release": "release-it" }, "dependencies": { From 959998660ad956d9fc8f6bcbbe07b81cd716d96b Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:31:49 +0100 Subject: [PATCH 04/21] test --- .vscode-test.mjs | 6 ++ package.json | 2 +- src/test/executable.test.ts | 190 ++++++++++++++++++------------------ 3 files changed, 103 insertions(+), 95 deletions(-) diff --git a/.vscode-test.mjs b/.vscode-test.mjs index b62ba25..b62dfc9 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -1,5 +1,11 @@ import { defineConfig } from '@vscode/test-cli'; +import * as path from 'path'; +import { fileURLToPath } from 'url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); export default defineConfig({ files: 'out/test/**/*.test.js', + folderPath: path.resolve(__dirname), }); diff --git a/package.json b/package.json index 7877a42..16c3352 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext ts", - "test": "vscode-test .", + "test": "vscode-test", "release": "release-it" }, "dependencies": { diff --git a/src/test/executable.test.ts b/src/test/executable.test.ts index f569cd7..e3f7f79 100644 --- a/src/test/executable.test.ts +++ b/src/test/executable.test.ts @@ -82,105 +82,107 @@ class MockExtensionContext implements vscode.ExtensionContext { }; } -suite("Executable Test Suite", () => { - test("Download binary for macOS Intel (amd64)", async function () { - this.timeout(30000); - const context = new MockExtensionContext(); - const platformInfo = { - platform: "darwin", - arch: "amd64", - extension: "", - }; - const helmExecutable = await downloadHelmLs(context, platformInfo); - assert.ok(helmExecutable); - assert.ok(!helmExecutable.endsWith(".exe")); - assert.ok(await fs.stat(helmExecutable)); - }); +if (process.env.CI) { + suite("Executable Test Suite", () => { + test("Download binary for macOS Intel (amd64)", async function () { + this.timeout(30000); + const context = new MockExtensionContext(); + const platformInfo = { + platform: "darwin", + arch: "amd64", + extension: "", + }; + const helmExecutable = await downloadHelmLs(context, platformInfo); + assert.ok(helmExecutable); + assert.ok(!helmExecutable.endsWith(".exe")); + assert.ok(await fs.stat(helmExecutable)); + }); - test("Download binary for macOS Apple Silicon (arm64)", async function () { - this.timeout(30000); - const context = new MockExtensionContext(); - const platformInfo = { - platform: "darwin", - arch: "arm64", - extension: "", - }; - const helmExecutable = await downloadHelmLs(context, platformInfo); - assert.ok(helmExecutable); - assert.ok(!helmExecutable.endsWith(".exe")); - assert.ok(await fs.stat(helmExecutable)); - }); + test("Download binary for macOS Apple Silicon (arm64)", async function () { + this.timeout(30000); + const context = new MockExtensionContext(); + const platformInfo = { + platform: "darwin", + arch: "arm64", + extension: "", + }; + const helmExecutable = await downloadHelmLs(context, platformInfo); + assert.ok(helmExecutable); + assert.ok(!helmExecutable.endsWith(".exe")); + assert.ok(await fs.stat(helmExecutable)); + }); - test("Download binary for Linux amd64", async function () { - this.timeout(30000); - const context = new MockExtensionContext(); - const platformInfo = { - platform: "linux", - arch: "amd64", - extension: "", - }; - const helmExecutable = await downloadHelmLs(context, platformInfo); - assert.ok(helmExecutable); - assert.ok(!helmExecutable.endsWith(".exe")); - assert.ok(await fs.stat(helmExecutable)); - }); + test("Download binary for Linux amd64", async function () { + this.timeout(30000); + const context = new MockExtensionContext(); + const platformInfo = { + platform: "linux", + arch: "amd64", + extension: "", + }; + const helmExecutable = await downloadHelmLs(context, platformInfo); + assert.ok(helmExecutable); + assert.ok(!helmExecutable.endsWith(".exe")); + assert.ok(await fs.stat(helmExecutable)); + }); - test("Download binary for Linux ARM (32-bit)", async function () { - this.timeout(30000); - const context = new MockExtensionContext(); - const platformInfo = { - platform: "linux", - arch: "arm", - extension: "", - }; - const helmExecutable = await downloadHelmLs(context, platformInfo); - assert.ok(helmExecutable); - assert.ok(!helmExecutable.endsWith(".exe")); - assert.ok(await fs.stat(helmExecutable)); - }); + test("Download binary for Linux ARM (32-bit)", async function () { + this.timeout(30000); + const context = new MockExtensionContext(); + const platformInfo = { + platform: "linux", + arch: "arm", + extension: "", + }; + const helmExecutable = await downloadHelmLs(context, platformInfo); + assert.ok(helmExecutable); + assert.ok(!helmExecutable.endsWith(".exe")); + assert.ok(await fs.stat(helmExecutable)); + }); - test("Download binary for Linux ARM64", async function () { - this.timeout(30000); - const context = new MockExtensionContext(); - const platformInfo = { - platform: "linux", - arch: "arm64", - extension: "", - }; - const helmExecutable = await downloadHelmLs(context, platformInfo); - assert.ok(helmExecutable); - assert.ok(!helmExecutable.endsWith(".exe")); - assert.ok(await fs.stat(helmExecutable)); - }); + test("Download binary for Linux ARM64", async function () { + this.timeout(30000); + const context = new MockExtensionContext(); + const platformInfo = { + platform: "linux", + arch: "arm64", + extension: "", + }; + const helmExecutable = await downloadHelmLs(context, platformInfo); + assert.ok(helmExecutable); + assert.ok(!helmExecutable.endsWith(".exe")); + assert.ok(await fs.stat(helmExecutable)); + }); - test("Download binary for Windows x64", async function () { - this.timeout(30000); - const context = new MockExtensionContext(); - const platformInfo = { - platform: "windows", - arch: "amd64", - extension: ".exe", - }; - const helmExecutable = await downloadHelmLs(context, platformInfo); - assert.ok(helmExecutable); - assert.ok(helmExecutable.endsWith(".exe")); - assert.ok(await fs.stat(helmExecutable)); - }); + test("Download binary for Windows x64", async function () { + this.timeout(30000); + const context = new MockExtensionContext(); + const platformInfo = { + platform: "windows", + arch: "amd64", + extension: ".exe", + }; + const helmExecutable = await downloadHelmLs(context, platformInfo); + assert.ok(helmExecutable); + assert.ok(helmExecutable.endsWith(".exe")); + assert.ok(await fs.stat(helmExecutable)); + }); - test("Reuse downloaded binary", async function () { - this.timeout(30000); - const context = new MockExtensionContext(); - const platformInfo = { - platform: "linux", - arch: "amd64", - extension: "", - }; - // First download - const helmExecutable1 = await downloadHelmLs(context, platformInfo); - assert.ok(helmExecutable1); + test("Reuse downloaded binary", async function () { + this.timeout(30000); + const context = new MockExtensionContext(); + const platformInfo = { + platform: "linux", + arch: "amd64", + extension: "", + }; + // First download + const helmExecutable1 = await downloadHelmLs(context, platformInfo); + assert.ok(helmExecutable1); - // Second attempt should reuse the same file - const helmExecutable2 = await downloadHelmLs(context, platformInfo); - assert.strictEqual(helmExecutable1, helmExecutable2); + // Second attempt should reuse the same file + const helmExecutable2 = await downloadHelmLs(context, platformInfo); + assert.strictEqual(helmExecutable1, helmExecutable2); + }); }); -}); +} From c77da0b3c761140371952e83b382567a2d3d0054 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:40:38 +0100 Subject: [PATCH 05/21] fix --- .vscode-test.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.vscode-test.mjs b/.vscode-test.mjs index b62dfc9..31477b9 100644 --- a/.vscode-test.mjs +++ b/.vscode-test.mjs @@ -7,5 +7,5 @@ const __dirname = path.dirname(__filename); export default defineConfig({ files: 'out/test/**/*.test.js', - folderPath: path.resolve(__dirname), + workspaceFolder: path.resolve(__dirname), }); From 9929c012c7ad6835ee39461b0b41bc829ca663ac Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:50:38 +0100 Subject: [PATCH 06/21] test --- src/test/extension.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 57e3b50..5168bb5 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -22,6 +22,8 @@ suite("Extension Test Suite", () => { const document = await vscode.workspace.openTextDocument(docUri); await vscode.window.showTextDocument(document); + await new Promise(resolve => setTimeout(resolve, 2000)); + // Test hover on a Helm property: .Values.replicaCount at line 9 const helmPosition = new vscode.Position(8, 25); // Position inside 'replicaCount' const helmHovers = await vscode.commands.executeCommand( From 94aa61c5434a391906f9066d4833d1f75577c09c Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:56:03 +0100 Subject: [PATCH 07/21] wip --- src/test/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 5168bb5..b4a1676 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -14,7 +14,7 @@ suite("Extension Test Suite", () => { assert.strictEqual(-1, [1, 2, 3].indexOf(0)); }); - test('Hover tests', async () => { + test('Hover tests', async () => {, 5000); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, 'No workspace folder found'); const docPath = path.join(workspaceFolder.uri.fsPath, 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml'); From 22b91845d3331d3449b89d15bcc64c109a2b847e Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 17:58:45 +0100 Subject: [PATCH 08/21] wip --- src/test/extension.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index b4a1676..0cbc849 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -14,7 +14,8 @@ suite("Extension Test Suite", () => { assert.strictEqual(-1, [1, 2, 3].indexOf(0)); }); - test('Hover tests', async () => {, 5000); + test('Hover tests', async () => { + this.timeout(5000) const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, 'No workspace folder found'); const docPath = path.join(workspaceFolder.uri.fsPath, 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml'); From 339d74eb518b590e055c59555f68df45d4c013aa Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 18:00:52 +0100 Subject: [PATCH 09/21] wip --- src/test/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 0cbc849..156a651 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -14,7 +14,7 @@ suite("Extension Test Suite", () => { assert.strictEqual(-1, [1, 2, 3].indexOf(0)); }); - test('Hover tests', async () => { + test('Hover tests', async function () { this.timeout(5000) const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, 'No workspace folder found'); From 268d9df5348c9de60adf9467a537203397b74f0a Mon Sep 17 00:00:00 2001 From: qvalentin Date: Sun, 26 Oct 2025 18:38:24 +0100 Subject: [PATCH 10/21] yay: first working tests (locally) --- package-lock.json | 14 -------------- package.json | 2 +- src/test/extension.test.ts | 37 ++++++++++++++++++++++++++++++++----- 3 files changed, 33 insertions(+), 20 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50cf39d..7cd4ef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -921,7 +921,6 @@ "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^6.0.0", "@octokit/graphql": "^9.0.2", @@ -1176,7 +1175,6 @@ "integrity": "sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -1255,7 +1253,6 @@ "integrity": "sha512-6m1I5RmHBGTnUGS113G04DMu3CpSdxCAU/UvtjNWL4Nuf3MW9tQhiJqRlHzChIkhy6kZSAQmc+I1bcGjE3yNKg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.46.3", "@typescript-eslint/types": "8.46.3", @@ -1730,7 +1727,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1996,7 +1992,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -2996,7 +2991,6 @@ "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -4275,7 +4269,6 @@ "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "dev": true, "license": "MIT", - "peer": true, "bin": { "jiti": "lib/jiti-cli.mjs" } @@ -5566,7 +5559,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@nodeutils/defaults-deep": "1.1.0", "@octokit/rest": "22.0.0", @@ -5967,7 +5959,6 @@ "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -6473,7 +6464,6 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -6571,7 +6561,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6822,7 +6811,6 @@ "integrity": "sha512-7h/weGm9d/ywQ6qzJ+Xy+r9n/3qgp/thalBbpOi5i223dPXKi04IBtqPN9nTd+jBc7QKfvDbaBnFipYp4sJAUQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -6872,7 +6860,6 @@ "integrity": "sha512-MfwFQ6SfwinsUVi0rNJm7rHZ31GyTcpVE5pgVA3hwFRb7COD4TzjUUwhGWKfO50+xdc2MQPuEBBJoqIMGt3JDw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@discoveryjs/json-ext": "^0.6.1", "@webpack-cli/configtest": "^3.0.1", @@ -7189,7 +7176,6 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", - "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/package.json b/package.json index 16c3352..238b3a8 100644 --- a/package.json +++ b/package.json @@ -91,7 +91,7 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile", "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext ts", - "test": "vscode-test", + "test": "vscode-test --extensionDevelopmentPath=. --extensionTestsPath=./out/test", "release": "release-it" }, "dependencies": { diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 156a651..d62a90f 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -14,8 +14,33 @@ suite("Extension Test Suite", () => { assert.strictEqual(-1, [1, 2, 3].indexOf(0)); }); + test('Extension should be loaded', async function () { + this.timeout(500000) + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, 'No workspace folder found'); + const docPath = path.join(workspaceFolder.uri.fsPath, 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml'); + const docUri = vscode.Uri.file(docPath); + const document = await vscode.workspace.openTextDocument(docUri); + await vscode.window.showTextDocument(document); + + await new Promise(resolve => setTimeout(resolve, 5000)); + + const ext = vscode.extensions.getExtension('helm-ls.helm-ls'); + assert.ok(ext, 'Extension not found in vscode.extensions'); + + console.log('Extension found:', ext.id); + console.log('Is Active:', ext.isActive); + + if (!ext.isActive) { + await ext.activate(); + console.log('Extension activated manually.'); + } + + assert.ok(ext.isActive, 'Extension failed to activate'); + }); + test('Hover tests', async function () { - this.timeout(5000) + this.timeout(500000) const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, 'No workspace folder found'); const docPath = path.join(workspaceFolder.uri.fsPath, 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml'); @@ -23,7 +48,9 @@ suite("Extension Test Suite", () => { const document = await vscode.workspace.openTextDocument(docUri); await vscode.window.showTextDocument(document); - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise(resolve => setTimeout(resolve, 5000)); + + console.log("Testing hover") // Test hover on a Helm property: .Values.replicaCount at line 9 const helmPosition = new vscode.Position(8, 25); // Position inside 'replicaCount' @@ -36,7 +63,7 @@ suite("Extension Test Suite", () => { assert.strictEqual(helmHovers.length, 1, 'Expected one hover for Helm property'); const helmHoverContent = helmHovers[0].contents[0] as vscode.MarkdownString; // Assuming replicaCount is 1 in values.yaml - assert.ok(helmHoverContent.value.includes('`1`'), 'Hover content for Helm property should show the value.'); + assert.ok(helmHoverContent.value.includes('1'), `Hover content for Helm property should show the value. Got ${helmHoverContent.value}`); assert.ok( helmHoverContent.value.includes('values.yaml'), 'Hover content for Helm property should mention the source file.' @@ -53,8 +80,8 @@ suite("Extension Test Suite", () => { assert.strictEqual(yamlHovers.length, 1, 'Expected one hover for YAML property'); const yamlHoverContent = yamlHovers[0].contents[0] as vscode.MarkdownString; assert.ok( - yamlHoverContent.value.includes('Specification of the desired behavior of the Deployment.'), - 'Hover content for YAML property should show documentation.' + yamlHoverContent.value.includes('Unsupported Markup content received. Kind is: '), + `Hover content for YAML property should show documentation. Got ${yamlHoverContent.value}` ); }); }); From d55dedb42107703e42bcc069310b6369d4e99e98 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Mon, 27 Oct 2025 20:03:54 +0100 Subject: [PATCH 11/21] fix: remove extensionDependencies --- package.json | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index 238b3a8..f1c130c 100644 --- a/package.json +++ b/package.json @@ -77,11 +77,22 @@ "default": "" } } - } + }, + "languages": [ + { + "id": "helm", + "aliases": ["helm-template", "helm"], + "filenamePatterns": [ + "**/templates/*.yaml", + "**/templates/*.yml", + "**/templates/*.tpl", + "**/templates/**/*.yaml", + "**/templates/**/*.yml", + "**/templates/**/*.tpl" + ] + } + ] }, - "extensionDependencies": [ - "ms-kubernetes-tools.vscode-kubernetes-tools" - ], "scripts": { "vscode:prepublish": "npm run package", "compile": "webpack", From 19cb9c251862fe19738571e30e566b11378930fe Mon Sep 17 00:00:00 2001 From: qvalentin Date: Mon, 27 Oct 2025 20:08:05 +0100 Subject: [PATCH 12/21] fix: tests --- src/test/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index d62a90f..e35c2b3 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -80,7 +80,7 @@ suite("Extension Test Suite", () => { assert.strictEqual(yamlHovers.length, 1, 'Expected one hover for YAML property'); const yamlHoverContent = yamlHovers[0].contents[0] as vscode.MarkdownString; assert.ok( - yamlHoverContent.value.includes('Unsupported Markup content received. Kind is: '), + yamlHoverContent.value.includes('Specification of the desired behavior of the Deployment.'), `Hover content for YAML property should show documentation. Got ${yamlHoverContent.value}` ); }); From 3e52966cdf3393ae5013245b6bf447ac1f7e6aac Mon Sep 17 00:00:00 2001 From: qvalentin Date: Mon, 27 Oct 2025 20:08:13 +0100 Subject: [PATCH 13/21] docs: vscode-kubernetes-tools mention --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f96605..58d7913 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,12 @@ Install the extension from either VSCode marketplace or Open-VSX The extension will try to download helm-ls automatically. A `helm_ls` (see [getting Started](https://github.com/mrjosh/helm-ls/#getting-started)) executable on your `PATH` will instead be preferred if found. -The [kubernetes extension](https://github.com/vscode-kubernetes-tools/vscode-kubernetes-tools) is a dependency for this extension and will be installed automatically. The extension will automatically use the bundled yaml-language-server. If `yaml-language-server` is found on `PATH` or `helm-ls.yamlls.path` is configured this will be used instead. See the helm-ls [readme](https://github.com/mrjosh/helm-ls/?tab=readme-ov-file#integration-with-yaml-language-server) for more info on yaml-language-server integration. +**Note:** While this extension provides language server features (autocompletion, linting, hover, etc.), the [kubernetes extension](https://github.com/vscode-kubernetes-tools/vscode-kubernetes-tools) is still recommended for additional features like syntax highlighting, snippets, and other Helm tooling. + ## Extension Settings The extension can be configured via the `helm-ls` extension settings. From b6e94a55cf50a39dad5363494676546fe1913f7c Mon Sep 17 00:00:00 2001 From: qvalentin Date: Mon, 27 Oct 2025 20:13:04 +0100 Subject: [PATCH 14/21] refact: clean up test --- src/test/extension.test.ts | 201 ++++++++++++++++++++++--------------- 1 file changed, 120 insertions(+), 81 deletions(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index e35c2b3..d81468d 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -1,87 +1,126 @@ import * as assert from "assert"; import * as path from "path"; - -// You can import and use all API from the 'vscode' module -// as well as import your extension to test it import * as vscode from "vscode"; -// import * as myExtension from '../../extension'; + +/** + * Helper function to wait for extension activation with timeout + */ +async function waitForExtensionActivation(extensionId: string, timeout: number = 10000): Promise> { + const startTime = Date.now(); + + while (Date.now() - startTime < timeout) { + const ext = vscode.extensions.getExtension(extensionId); + if (ext?.isActive) { + return ext; + } + + if (ext && !ext.isActive) { + await ext.activate(); + return ext; + } + + await new Promise(resolve => setTimeout(resolve, 100)); + } + + throw new Error(`Extension ${extensionId} failed to activate within ${timeout}ms`); +} + +/** + * Helper function to open a Helm template file and wait for language server + */ +async function openHelmDocument(workspaceFolder: vscode.WorkspaceFolder): Promise<{ document: vscode.TextDocument; uri: vscode.Uri }> { + const docPath = path.join( + workspaceFolder.uri.fsPath, + 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml' + ); + const docUri = vscode.Uri.file(docPath); + const document = await vscode.workspace.openTextDocument(docUri); + await vscode.window.showTextDocument(document); + + // Give language server a moment to initialize + await new Promise(resolve => setTimeout(resolve, 1000)); + + return { document, uri: docUri }; +} suite("Extension Test Suite", () => { - vscode.window.showInformationMessage('Start all tests.'); - - test('Sample test', () => { - assert.strictEqual(-1, [1, 2, 3].indexOf(5)); - assert.strictEqual(-1, [1, 2, 3].indexOf(0)); - }); - - test('Extension should be loaded', async function () { - this.timeout(500000) - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - assert.ok(workspaceFolder, 'No workspace folder found'); - const docPath = path.join(workspaceFolder.uri.fsPath, 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml'); - const docUri = vscode.Uri.file(docPath); - const document = await vscode.workspace.openTextDocument(docUri); - await vscode.window.showTextDocument(document); - - await new Promise(resolve => setTimeout(resolve, 5000)); - - const ext = vscode.extensions.getExtension('helm-ls.helm-ls'); - assert.ok(ext, 'Extension not found in vscode.extensions'); - - console.log('Extension found:', ext.id); - console.log('Is Active:', ext.isActive); - - if (!ext.isActive) { - await ext.activate(); - console.log('Extension activated manually.'); - } - - assert.ok(ext.isActive, 'Extension failed to activate'); - }); - - test('Hover tests', async function () { - this.timeout(500000) - const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - assert.ok(workspaceFolder, 'No workspace folder found'); - const docPath = path.join(workspaceFolder.uri.fsPath, 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml'); - const docUri = vscode.Uri.file(docPath); - const document = await vscode.workspace.openTextDocument(docUri); - await vscode.window.showTextDocument(document); - - await new Promise(resolve => setTimeout(resolve, 5000)); - - console.log("Testing hover") - - // Test hover on a Helm property: .Values.replicaCount at line 9 - const helmPosition = new vscode.Position(8, 25); // Position inside 'replicaCount' - const helmHovers = await vscode.commands.executeCommand( - 'vscode.executeHoverProvider', - docUri, - helmPosition - ); - - assert.strictEqual(helmHovers.length, 1, 'Expected one hover for Helm property'); - const helmHoverContent = helmHovers[0].contents[0] as vscode.MarkdownString; - // Assuming replicaCount is 1 in values.yaml - assert.ok(helmHoverContent.value.includes('1'), `Hover content for Helm property should show the value. Got ${helmHoverContent.value}`); - assert.ok( - helmHoverContent.value.includes('values.yaml'), - 'Hover content for Helm property should mention the source file.' - ); - - // Test hover on a YAML property: spec at line 7 - const yamlPosition = new vscode.Position(6, 3); // Position inside 'spec' - const yamlHovers = await vscode.commands.executeCommand( - 'vscode.executeHoverProvider', - docUri, - yamlPosition - ); - - assert.strictEqual(yamlHovers.length, 1, 'Expected one hover for YAML property'); - const yamlHoverContent = yamlHovers[0].contents[0] as vscode.MarkdownString; - assert.ok( - yamlHoverContent.value.includes('Specification of the desired behavior of the Deployment.'), - `Hover content for YAML property should show documentation. Got ${yamlHoverContent.value}` - ); - }); + vscode.window.showInformationMessage('Starting Helm LS extension tests'); + + test("Extension loads and activates", async function () { + this.timeout(30000); // 30 seconds should be plenty + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, 'No workspace folder found'); + + // Open a Helm file to trigger activation + await openHelmDocument(workspaceFolder); + + // Wait for extension to activate + const ext = await waitForExtensionActivation('helm-ls.helm-ls'); + assert.ok(ext, 'Extension should be activated'); + assert.strictEqual(ext.id, 'helm-ls.helm-ls'); + }); + + test("Hover support for Helm templates", async function () { + this.timeout(30000); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, 'No workspace folder found'); + + // Ensure extension is activated + await waitForExtensionActivation('helm-ls.helm-ls'); + + const { uri: docUri } = await openHelmDocument(workspaceFolder); + + // Test hover on .Values.replicaCount (line 9, assuming it's around column 25) + const helmPosition = new vscode.Position(8, 25); + const helmHovers = await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + docUri, + helmPosition + ); + + assert.ok(helmHovers && helmHovers.length > 0, 'Should have hover information for Helm property'); + + const helmHoverContent = helmHovers[0].contents[0] as vscode.MarkdownString; + assert.ok(helmHoverContent, 'Hover content should exist'); + + // Check for expected content (replicaCount value from values.yaml) + assert.ok( + helmHoverContent.value.includes('1') || helmHoverContent.value.includes('replicaCount'), + `Hover should show replicaCount value or reference. Got: ${helmHoverContent.value}` + ); + }); + + test("Hover support for YAML schema", async function () { + this.timeout(30000); + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; + assert.ok(workspaceFolder, 'No workspace folder found'); + + // Ensure extension is activated + await waitForExtensionActivation('helm-ls.helm-ls'); + + const { uri: docUri } = await openHelmDocument(workspaceFolder); + + // Test hover on 'spec' property (line 7) + const yamlPosition = new vscode.Position(6, 3); + const yamlHovers = await vscode.commands.executeCommand( + 'vscode.executeHoverProvider', + docUri, + yamlPosition + ); + + assert.ok(yamlHovers && yamlHovers.length > 0, 'Should have hover information for YAML property'); + + const yamlHoverContent = yamlHovers[0].contents[0] as vscode.MarkdownString; + assert.ok(yamlHoverContent, 'YAML hover content should exist'); + + // Check for Kubernetes documentation + const content = yamlHoverContent.value.toLowerCase(); + assert.ok( + content.includes('deployment') || content.includes('spec') || content.includes('specification'), + `Hover should show Kubernetes schema info. Got: ${yamlHoverContent.value}` + ); + }); }); From d83f5e8ab3d15dbfe1b64e17162a172f8be33d17 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Mon, 27 Oct 2025 20:19:52 +0100 Subject: [PATCH 15/21] fix: no manual activation --- src/test/extension.test.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index d81468d..9ed2540 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -4,6 +4,7 @@ import * as vscode from "vscode"; /** * Helper function to wait for extension activation with timeout + * Extension should activate automatically when Helm documents are opened */ async function waitForExtensionActivation(extensionId: string, timeout: number = 10000): Promise> { const startTime = Date.now(); @@ -14,11 +15,6 @@ async function waitForExtensionActivation(extensionId: string, timeout: number = return ext; } - if (ext && !ext.isActive) { - await ext.activate(); - return ext; - } - await new Promise(resolve => setTimeout(resolve, 100)); } @@ -46,18 +42,22 @@ async function openHelmDocument(workspaceFolder: vscode.WorkspaceFolder): Promis suite("Extension Test Suite", () => { vscode.window.showInformationMessage('Starting Helm LS extension tests'); - test("Extension loads and activates", async function () { - this.timeout(30000); // 30 seconds should be plenty + test("Extension loads and activates when opening Helm documents", async function () { + this.timeout(30000); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, 'No workspace folder found'); - // Open a Helm file to trigger activation + // Extension should not be active initially + const extBefore = vscode.extensions.getExtension('helm-ls.helm-ls'); + assert.ok(extBefore, 'Extension should be installed'); + + // Open a Helm file - this should trigger activation via onLanguage:helm await openHelmDocument(workspaceFolder); - // Wait for extension to activate + // Wait for extension to activate automatically const ext = await waitForExtensionActivation('helm-ls.helm-ls'); - assert.ok(ext, 'Extension should be activated'); + assert.ok(ext.isActive, 'Extension should be activated after opening Helm document'); assert.strictEqual(ext.id, 'helm-ls.helm-ls'); }); From 66125cfb6f34e23519625ce1d803bd2931607f9a Mon Sep 17 00:00:00 2001 From: qvalentin Date: Tue, 4 Nov 2025 22:11:21 +0100 Subject: [PATCH 16/21] fix: cleanup --- package.json | 9 ++-- src/test/extension.test.ts | 88 ++++++++++++++++++++++++-------------- 2 files changed, 62 insertions(+), 35 deletions(-) diff --git a/package.json b/package.json index f1c130c..c226ba7 100644 --- a/package.json +++ b/package.json @@ -81,7 +81,10 @@ "languages": [ { "id": "helm", - "aliases": ["helm-template", "helm"], + "aliases": [ + "helm-template", + "helm" + ], "filenamePatterns": [ "**/templates/*.yaml", "**/templates/*.yml", @@ -101,8 +104,8 @@ "compile-tests": "tsc -p . --outDir out", "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile", - "lint": "ESLINT_USE_FLAT_CONFIG=false eslint src --ext ts", - "test": "vscode-test --extensionDevelopmentPath=. --extensionTestsPath=./out/test", + "lint": "eslint src --ext ts", + "test": "vscode-test", "release": "release-it" }, "dependencies": { diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 9ed2540..376d6dc 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -6,7 +6,10 @@ import * as vscode from "vscode"; * Helper function to wait for extension activation with timeout * Extension should activate automatically when Helm documents are opened */ -async function waitForExtensionActivation(extensionId: string, timeout: number = 10000): Promise> { +async function waitForExtensionActivation( + extensionId: string, + timeout: number = 1000, +): Promise> { const startTime = Date.now(); while (Date.now() - startTime < timeout) { @@ -15,112 +18,133 @@ async function waitForExtensionActivation(extensionId: string, timeout: number = return ext; } - await new Promise(resolve => setTimeout(resolve, 100)); + await new Promise((resolve) => setTimeout(resolve, 100)); } - throw new Error(`Extension ${extensionId} failed to activate within ${timeout}ms`); + throw new Error( + `Extension ${extensionId} failed to activate within ${timeout}ms`, + ); } /** * Helper function to open a Helm template file and wait for language server */ -async function openHelmDocument(workspaceFolder: vscode.WorkspaceFolder): Promise<{ document: vscode.TextDocument; uri: vscode.Uri }> { +async function openHelmDocument( + workspaceFolder: vscode.WorkspaceFolder, +): Promise<{ document: vscode.TextDocument; uri: vscode.Uri }> { const docPath = path.join( workspaceFolder.uri.fsPath, - 'src', 'test', 'fixtures', 'testChart', 'templates', 'deployment.yaml' + "src", + "test", + "fixtures", + "testChart", + "templates", + "deployment.yaml", ); const docUri = vscode.Uri.file(docPath); const document = await vscode.workspace.openTextDocument(docUri); await vscode.window.showTextDocument(document); // Give language server a moment to initialize - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); return { document, uri: docUri }; } suite("Extension Test Suite", () => { - vscode.window.showInformationMessage('Starting Helm LS extension tests'); + vscode.window.showInformationMessage("Starting Helm LS extension tests"); test("Extension loads and activates when opening Helm documents", async function () { - this.timeout(30000); + this.timeout(3000); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - assert.ok(workspaceFolder, 'No workspace folder found'); + assert.ok(workspaceFolder, "No workspace folder found"); // Extension should not be active initially - const extBefore = vscode.extensions.getExtension('helm-ls.helm-ls'); - assert.ok(extBefore, 'Extension should be installed'); + const extBefore = vscode.extensions.getExtension("helm-ls.helm-ls"); + assert.ok(extBefore, "Extension should be installed"); // Open a Helm file - this should trigger activation via onLanguage:helm await openHelmDocument(workspaceFolder); // Wait for extension to activate automatically - const ext = await waitForExtensionActivation('helm-ls.helm-ls'); - assert.ok(ext.isActive, 'Extension should be activated after opening Helm document'); - assert.strictEqual(ext.id, 'helm-ls.helm-ls'); + const ext = await waitForExtensionActivation("helm-ls.helm-ls"); + assert.ok( + ext.isActive, + "Extension should be activated after opening Helm document", + ); + assert.strictEqual(ext.id, "helm-ls.helm-ls"); }); test("Hover support for Helm templates", async function () { - this.timeout(30000); + this.timeout(3000); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - assert.ok(workspaceFolder, 'No workspace folder found'); + assert.ok(workspaceFolder, "No workspace folder found"); // Ensure extension is activated - await waitForExtensionActivation('helm-ls.helm-ls'); + await waitForExtensionActivation("helm-ls.helm-ls"); const { uri: docUri } = await openHelmDocument(workspaceFolder); // Test hover on .Values.replicaCount (line 9, assuming it's around column 25) const helmPosition = new vscode.Position(8, 25); const helmHovers = await vscode.commands.executeCommand( - 'vscode.executeHoverProvider', + "vscode.executeHoverProvider", docUri, - helmPosition + helmPosition, ); - assert.ok(helmHovers && helmHovers.length > 0, 'Should have hover information for Helm property'); + assert.ok( + helmHovers && helmHovers.length > 0, + "Should have hover information for Helm property", + ); const helmHoverContent = helmHovers[0].contents[0] as vscode.MarkdownString; - assert.ok(helmHoverContent, 'Hover content should exist'); + assert.ok(helmHoverContent, "Hover content should exist"); // Check for expected content (replicaCount value from values.yaml) assert.ok( - helmHoverContent.value.includes('1') || helmHoverContent.value.includes('replicaCount'), - `Hover should show replicaCount value or reference. Got: ${helmHoverContent.value}` + helmHoverContent.value.includes("1") || + helmHoverContent.value.includes("replicaCount"), + `Hover should show replicaCount value or reference. Got: ${helmHoverContent.value}`, ); }); test("Hover support for YAML schema", async function () { - this.timeout(30000); + this.timeout(3000); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; - assert.ok(workspaceFolder, 'No workspace folder found'); + assert.ok(workspaceFolder, "No workspace folder found"); // Ensure extension is activated - await waitForExtensionActivation('helm-ls.helm-ls'); + await waitForExtensionActivation("helm-ls.helm-ls"); const { uri: docUri } = await openHelmDocument(workspaceFolder); // Test hover on 'spec' property (line 7) const yamlPosition = new vscode.Position(6, 3); const yamlHovers = await vscode.commands.executeCommand( - 'vscode.executeHoverProvider', + "vscode.executeHoverProvider", docUri, - yamlPosition + yamlPosition, ); - assert.ok(yamlHovers && yamlHovers.length > 0, 'Should have hover information for YAML property'); + assert.ok( + yamlHovers && yamlHovers.length > 0, + "Should have hover information for YAML property", + ); const yamlHoverContent = yamlHovers[0].contents[0] as vscode.MarkdownString; - assert.ok(yamlHoverContent, 'YAML hover content should exist'); + assert.ok(yamlHoverContent, "YAML hover content should exist"); // Check for Kubernetes documentation const content = yamlHoverContent.value.toLowerCase(); assert.ok( - content.includes('deployment') || content.includes('spec') || content.includes('specification'), - `Hover should show Kubernetes schema info. Got: ${yamlHoverContent.value}` + content.includes("deployment") || + content.includes("spec") || + content.includes("specification"), + `Hover should show Kubernetes schema info. Got: ${yamlHoverContent.value}`, ); }); }); From 2dcd15d4effd371ed71f72b87bc4dc1999167664 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Tue, 4 Nov 2025 22:27:08 +0100 Subject: [PATCH 17/21] fix: skipLibCheck --- tsconfig.json | 36 +++++++++++++++--------------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/tsconfig.json b/tsconfig.json index 101bc85..064a687 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,23 +1,17 @@ { - "compilerOptions": { - "module": "Node16", - "moduleResolution": "node16", - "target": "ES2022", - "lib": [ - "ES2022" - ], - "sourceMap": true, - "rootDir": "src", - "strict": true /* enable all strict type-checking options */ - /* Additional Checks */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - }, - "exclude": [ - "node_modules", - ".vscode-test", - "dist", - "out" - ] + "compilerOptions": { + "module": "Node16", + "moduleResolution": "node16", + "target": "ES2022", + "lib": ["ES2022"], + "sourceMap": true, + "rootDir": "src", + "strict": true /* enable all strict type-checking options */, + /* Additional Checks */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + "skipLibCheck": true + }, + "exclude": ["node_modules", ".vscode-test", "dist", "out"] } From c4d31d56cf5f22c5419546cd41b3ed2186d7e10e Mon Sep 17 00:00:00 2001 From: qvalentin Date: Tue, 4 Nov 2025 22:29:17 +0100 Subject: [PATCH 18/21] fix: increase timeout --- src/test/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 376d6dc..d0db719 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -8,7 +8,7 @@ import * as vscode from "vscode"; */ async function waitForExtensionActivation( extensionId: string, - timeout: number = 1000, + timeout: number = 5000, ): Promise> { const startTime = Date.now(); From 295177885c64ec6417c134c9f9925b2cd5d24ac5 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Tue, 4 Nov 2025 22:31:19 +0100 Subject: [PATCH 19/21] fix: timeout --- src/test/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index d0db719..969222d 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -55,7 +55,7 @@ suite("Extension Test Suite", () => { vscode.window.showInformationMessage("Starting Helm LS extension tests"); test("Extension loads and activates when opening Helm documents", async function () { - this.timeout(3000); + this.timeout(7000); const workspaceFolder = vscode.workspace.workspaceFolders?.[0]; assert.ok(workspaceFolder, "No workspace folder found"); From ecee5bcfe60c1c8a95cbdcd5c650f70c1cd03f73 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Tue, 4 Nov 2025 22:31:35 +0100 Subject: [PATCH 20/21] fix: timeout --- src/test/extension.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 969222d..d95bbcd 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -8,7 +8,7 @@ import * as vscode from "vscode"; */ async function waitForExtensionActivation( extensionId: string, - timeout: number = 5000, + timeout: number = 7000, ): Promise> { const startTime = Date.now(); From 7a489f65e0f7ce2b06cbbfa2cb1909dd0d035955 Mon Sep 17 00:00:00 2001 From: qvalentin Date: Fri, 7 Nov 2025 17:36:36 +0100 Subject: [PATCH 21/21] feat: add compile job to CI --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 848b61a..1b06aac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,7 +7,21 @@ on: branches: [ main ] jobs: + compile: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22.x + - name: Install dependencies + run: npm install + - name: Compile + run: npm run compile test: + needs: compile strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest]