Files
languageservices/languageservice/src/complete.expressions.test.ts
T
Christopher Schleiden b82ddcdb42 Update import paths
String replacement from `@github/actions-` to `@actions/`
2023-03-23 09:57:18 -07:00

1286 lines
32 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-non-null-assertion */
import {data, DescriptionDictionary} from "@actions/expressions";
import {CompletionItem, CompletionItemKind} from "vscode-languageserver-types";
import {complete, getExpressionInput} from "./complete";
import {ContextProviderConfig} from "./context-providers/config";
import {registerLogger} from "./log";
import {getPositionFromCursor} from "./test-utils/cursor-position";
import {TestLogger} from "./test-utils/logger";
import {testFileProvider} from "./test-utils/test-file-provider";
import {clearCache} from "./utils/workflow-cache";
const contextProviderConfig: ContextProviderConfig = {
getContext: (context: string) => {
switch (context) {
case "github":
return Promise.resolve(
new DescriptionDictionary({
key: "event",
value: new data.StringData("push"),
description: "The event that triggered the workflow"
})
);
}
return Promise.resolve(undefined);
}
};
registerLogger(new TestLogger());
beforeEach(() => {
clearCache();
});
describe("expressions", () => {
it("input extraction", () => {
const test = (input: string) => {
const [doc, pos] = getPositionFromCursor(input);
return getExpressionInput(doc.getText(), pos.character);
};
// With ${{ }}
expect(test("${{ gh |")).toBe(" gh ");
expect(test("${{ gh |}}")).toBe(" gh ");
expect(test("${{ vars| == 'test' }}")).toBe(" vars");
expect(test("${{ fromJso|('test').bar == 'test' }}")).toBe(" fromJso");
expect(test("${{ github.| == 'test' }}")).toBe(" github.");
expect(test("test ${{ github.| == 'test' }}")).toBe(" github.");
expect(test("${{ vars }} ${{ gh |}}")).toBe(" gh ");
expect(test("${{ test.|")).toBe(" test.");
expect(test("${{ test.| }}")).toBe(" test.");
expect(test("${{ 1 == (test.|)")).toBe(" 1 == (test.");
// Without ${{ }}
expect(test("gh |")).toBe("gh ");
expect(test("gh |}}")).toBe("gh ");
expect(test("vars| == 'test' }}")).toBe("vars");
expect(test("fromJso|('test').bar == 'test' }}")).toBe("fromJso");
expect(test("github.| == 'test' }}")).toBe("github.");
expect(test("github.| == 'test' }}")).toBe("github.");
expect(test("test.|")).toBe("test.");
expect(test("test.| }}")).toBe("test.");
expect(test("1 == (test.|)")).toBe("1 == (test.");
});
describe("top-level auto-complete", () => {
it("single region", async () => {
const input = "run-name: ${{ | }}";
const result = await complete(...getPositionFromCursor(input));
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("within parentheses", async () => {
const result = await complete(...getPositionFromCursor("run-name: ${{ 1 == (github.|) }}"), {
contextProviderConfig
});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("contains description", async () => {
const input = "run-name: ${{ github.| }}";
const result = await complete(...getPositionFromCursor(input));
expect(result).toContainEqual<CompletionItem>({
label: "api_url",
documentation: {
kind: "markdown",
value: "The URL of the GitHub Actions REST API."
},
kind: CompletionItemKind.Variable
});
});
it("single region with existing input", async () => {
const input = "run-name: ${{ g| }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("single region with existing condition", async () => {
const input = "run-name: ${{ g| == 'test' }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("multiple regions with partial function", async () => {
const input = "run-name: Run a ${{ inputs.test }} one-line script ${{ from|('test') == inputs.name }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("multiple regions - first region", async () => {
const input = "run-name: test-${{ git| == 1 }}-${{ github.event }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("multiple regions", async () => {
const input = "run-name: test-${{ github }}-${{ | }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual([
"github",
"inputs",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
describe("multi-line strings", () => {
it("indented |", async () => {
const input = `on: push
jobs:
build:
steps:
- run: |
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input, 1), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("indented |+", async () => {
const input = `on: push
jobs:
build:
steps:
- run: |+
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input, 1), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("indented >", async () => {
const input = `on: push
jobs:
build:
steps:
- run: >
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("indented >+", async () => {
const input = `on: push
jobs:
build:
steps:
- run: >+
first line
test \${{ github.| }}
test2`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
});
it("nested with parentheses", async () => {
const input = `on:
workflow_dispatch:
inputs:
test:
type: string
jobs:
build:
runs-on: ubuntu-latest
env:
foo: '{}'
steps:
- name: "\${{ fromJSON('test') == (inputs.|) }}"`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["test"]);
});
it("nested auto-complete", async () => {
const input = "run-name: ${{ github.| }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("auto-complete partial", async () => {
const input = "run-name: ${{ github.ev| }}";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("auto-complete complex partial", async () => {
const input = 'run-name: "run ${{ github.ev| }} run"';
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("using default context provider", async () => {
const input =
"on: push\njobs:\n build:\n runs-on: ubuntu-latest\n environment:\n url: ${{ runner.| }}\n steps:\n - run: echo";
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["arch", "name", "os", "temp", "tool_cache"]);
});
describe("job if", () => {
describe("without ${{", () => {
it("simple", async () => {
const input = `on: push
jobs:
build:
if: github.|
runs-on: ubuntu-latest
steps:
- run: echo`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("complex", async () => {
const input = `on: push
jobs:
build:
if: false && github.| == 'some-repo'
runs-on: ubuntu-latest
steps:
- run: echo`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
});
describe("with ${{", () => {
it("simple", async () => {
const input = `on: push
jobs:
build:
if: \${{ github.| }}
runs-on: ubuntu-latest
steps:
- run: echo`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("complex", async () => {
const input = `on: push
jobs:
build:
if: \${{ false && github.| == 'some-repo' }}
runs-on: ubuntu-latest
steps:
- run: echo`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
});
});
describe("step if", () => {
it("with ${{", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo
if: \${{ github.| }}`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
it("without ${{", async () => {
const input = `on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: echo
if: github.|`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["event"]);
});
});
});
it("context inherited from parent", async () => {
// The token definition is just a `string` and
// the parent `step-with` holds the allowed context
const input = `
on: push
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/deploy@v100
with:
deploy-key: \${{ secrets.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["GITHUB_TOKEN"]);
});
it("needs context only includes referenced jobs", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo hello b
c:
needs: [b]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["b"]);
});
it("needs.<job_id>", async () => {
const input = `
on: push
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.a.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["outputs", "result"]);
});
it("needs.<job_id>.outputs includes outputs", async () => {
const input = `
on: push
jobs:
a:
outputs:
build_id: my-build-id
runs-on: ubuntu-latest
steps:
- run: echo hello a
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.a.outputs.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["build_id"]);
});
it("env steps context only for current step and job", async () => {
const input = `
on: push
env:
envwf: workflow_env
jobs:
a:
runs-on: ubuntu-latest
env:
envjoba: job_a_env
b:
runs-on: ubuntu-latest
env:
envjobb: job_b_env
steps:
- name: step a
run: echo "hello"
env:
envstepa: step_a_env
- name: step b
run: echo "hello \${{ env.|
env:
envstepb: step_b_env
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["envjobb", "envstepb", "envwf"]);
});
it("inputs", async () => {
const input = `
on:
workflow_dispatch:
inputs:
name:
type: string
default: some value
another-name:
type: string
workflow_call:
inputs:
name:
type: string
default: value
third-name:
type: boolean
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ inputs.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]);
});
it("no inputs", async () => {
const input = `
on:
workflow_dispatch:
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ inputs.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result).toEqual([]);
});
it("secrets", async () => {
const input = `
on:
workflow_call:
secrets:
secret1:
required: true
description: "first secret"
secret2:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/deploy@v100
with:
deploy-key: \${{ secrets.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["GITHUB_TOKEN", "secret1", "secret2"]);
});
describe("github context", () => {
it("includes expected keys", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo \${{ github.| }}
`;
const result = await complete(...getPositionFromCursor(input));
expect(result.map(x => x.label)).toContain("actor");
});
it("includes event inputs", async () => {
const input = `
on:
workflow_dispatch:
inputs:
name:
type: string
default: some value
another-name:
type: string
workflow_call:
inputs:
name:
type: string
default: value
third-name:
type: boolean
jobs:
a:
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ github.event.inputs.|
`;
const result = await complete(...getPositionFromCursor(input));
expect(result.map(x => x.label)).toEqual(["another-name", "name", "third-name"]);
});
it("excludes event inputs and cron when no relevant events", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo \${{ github.event.| }}
`;
const result = await complete(...getPositionFromCursor(input));
expect(result.map(x => x.label)).not.toContain("inputs");
expect(result.map(x => x.label)).not.toContain("schedule");
});
it("includes cron schedules", async () => {
const input = `
on:
schedule:
- cron: '0 0 * * *'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo \${{ github.event.| }}
`;
const result = await complete(...getPositionFromCursor(input));
expect(result.map(x => x.label)).toEqual(["repository", "schedule", "workflow"]);
});
it("includes event payload", async () => {
const input = `
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo \${{ github.event.| }}
`;
const result = await complete(...getPositionFromCursor(input));
// forced is part of the push event payload
expect(result.map(x => x.label)).toContain("forced");
// pull_request is part of the pull_request event payload
expect(result.map(x => x.label)).toContain("pull_request");
});
it("includes event payload for workflow_call", async () => {
const input = `
on: [workflow_call]
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo \${{ github.event.| }}
`;
const result = await complete(...getPositionFromCursor(input));
// We don't validate github.event for workflow_call,
// but there should still be auto-completion suggestions
expect(result.length).toBeGreaterThan(0);
});
});
describe("steps context", () => {
it("includes defined step IDs", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo hello b
- id: c
run: echo "hello \${{ steps.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["a", "b"]);
});
it("step.<step_id>", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- run: echo "hello \${{ steps.a.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["conclusion", "outcome", "outputs"]);
});
it("ignores IDs from later steps", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
- id: b
run: echo "hello \${{ steps.|
- id: c
run: echo hello c
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["a"]);
});
it("Ignores generated IDs", async () => {
const input = `
on: push
jobs:
one:
runs-on: ubuntu-latest
steps:
- run: echo hello a
- id: b
run: echo hello b
- run: echo "hello \${{ steps.|
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["b"]);
});
});
describe("jobs context", () => {
it("includes defined jobs", async () => {
const input = `
on:
workflow_call:
# Map the workflow outputs to job outputs
outputs:
result:
description: "one or two"
value: \${{ jobs.| }}
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
two:
runs-on: ubuntu-latest
steps:
- id: b
run: echo hello b
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["one", "two"]);
});
it("jobs.<job_id>.result only", async () => {
const input = `
on:
workflow_call:
# Map the workflow outputs to job outputs
outputs:
result:
description: "The result"
value: \${{ jobs.one.| }}
jobs:
one:
runs-on: ubuntu-latest
steps:
- id: a
run: echo hello a
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["result"]);
});
it("jobs.<job_id>", async () => {
const input = `
on:
workflow_call:
outputs:
output1:
description: "A greeting"
value: \${{ jobs.example_job.| }}
jobs:
example_job:
name: Generate output
runs-on: ubuntu-latest
# Map the job outputs to step outputs
outputs:
output1: "\${{ steps.a.outputs.greeting }}"
steps:
- id: a
run: echo "greeting=hello" >> $GITHUB_OUTPUT
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["outputs", "result"]);
});
it("jobs.<job_id>.outputs", async () => {
const input = `
on:
workflow_call:
outputs:
output1:
description: "A greeting"
value: \${{ jobs.example_job.outputs.| }}
jobs:
example_job:
name: Generate output
runs-on: ubuntu-latest
# Map the job outputs to step outputs
outputs:
output1: "\${{ steps.a.outputs.greeting }}"
output2: "\${{ steps.b.outputs.greeting }}"
steps:
- id: a
run: echo "greeting=hello" >> $GITHUB_OUTPUT
- id: b
run: echo "greeting=hello" >> $GITHUB_OUTPUT
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["output1", "output2"]);
});
});
describe("strategy context", () => {
it("strategy is not suggested when outside of a matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).not.toContain("strategy");
});
it("strategy is suggested within a matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [1, 2]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toContain("strategy");
});
it("includes expected keys", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [1, 2]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ strategy.| }}.txt
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["fail-fast", "job-index", "job-total", "max-parallel"]);
});
});
describe("matrix context", () => {
it("matrix is not suggested when outside of a matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).not.toContain("strategy");
});
it("matrix is suggested within a matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
test-group: [1, 2]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- run: npm test > test-job-\${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toContain("strategy");
});
it("basic matrix job", async () => {
const input = `
on: push
jobs:
test:
runs-on: \${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
node: [14, 16]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["node", "os"]);
});
it("matrix with include", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include:
- color: green
- color: pink
animal: cat
- fruit: apple
shape: circle
- fruit: banana
- fruit: banana
animal: cat
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["animal", "color", "fruit", "shape"]);
});
it("matrix from expression", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix: \${{ fromJSON('{"color":["green","blue"]}') }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual([]);
});
it("matrix with include expression", async () => {
const input = `
on: push
jobs:
test:
runs-on: \${{ matrix.os }}
strategy:
matrix:
fruit: [apple, pear]
animal: [cat, dog]
include: \${{ fromJSON('{"color":"green"}') }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["animal", "fruit"]);
});
it("matrix with expression in property", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
color: \${{ fromJSON('["green","blue"]') }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: \${{ matrix.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["color"]);
});
});
describe("job context", () => {
it("job context is suggested within a job", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:14.16
services:
nginx:
image: node:14.16
steps:
- run: echo \${{ job.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["container", "services", "status"]);
});
it("job context is suggested within a job output", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
outputs:
environment: \${{ | }}
steps:
- id: a
run: echo hi
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual([
"env",
"github",
"inputs",
"job",
"needs",
"runner",
"secrets",
"steps",
"vars",
"contains",
"endsWith",
"format",
"fromJson",
"join",
"startsWith",
"toJson"
]);
});
it("step context is suggested within a job output", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
outputs:
environment: \${{ steps.| }}
steps:
- id: foo
run: echo hi
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["foo"]);
});
it("container context is suggested within a job container", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
container:
image: node:14.16
steps:
- run: echo \${{ job.container.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["id", "network"]);
});
it("services are suggested within a job services list", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
services:
nginx:
image: node:14.16
redis:
image: redis
steps:
- run: echo \${{ job.services.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["nginx", "redis"]);
});
it("services context is suggested within a job service", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
services:
nginx:
image: node:14.16
ports:
- 80:8080
- 90
steps:
- run: echo \${{ job.services.nginx.| }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.map(x => x.label)).toEqual(["id", "network", "ports"]);
});
});
it("context completion items include kind and insert text", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- run: echo \${{ | }}.txt
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
// Built-in function
const toJSON = result.find(x => x.label === "toJson");
expect(toJSON).toBeDefined();
expect(toJSON!.kind).toBe(CompletionItemKind.Function);
expect(toJSON!.insertText).toBe("toJson()");
// Function from context
const hashFiles = result.find(x => x.label === "hashFiles");
expect(hashFiles).toBeDefined();
expect(hashFiles!.kind).toBe(CompletionItemKind.Function);
expect(hashFiles!.insertText).toBe("hashFiles()");
// Not a function
const github = result.find(x => x.label === "github");
expect(github).toBeDefined();
expect(github!.kind).toBe(CompletionItemKind.Variable);
expect(github!.insertText).toBeUndefined();
});
it("function parentheses are not inserted when parentheses already exist", async () => {
const input = `
on: push
jobs:
test:
runs-on: ubuntu-latest
steps:
- run: echo \${{ toJS|(github.event) }}
`;
const result = await complete(...getPositionFromCursor(input), {contextProviderConfig});
expect(result.find(x => x.label === "toJson")!.insertText).toBe("toJson");
});
it("Parsing errors don't prevent reusable workflows from being loaded", async () => {
const input = `
on: push
jobs:
a:
uses: ./reusable-workflow-with-outputs.yaml
b:
needs: [a]
runs-on: ubuntu-latest
steps:
- run: echo "hello \${{ needs.a.outputs.| }}"
`;
const result = await complete(...getPositionFromCursor(input), {
contextProviderConfig,
fileProvider: testFileProvider
});
expect(result.map(x => x.label)).toEqual(["build_id"]);
});
});