Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 1 addition & 15 deletions .github/workflows/e2e.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,13 +32,8 @@ jobs:
strategy:
fail-fast: false
matrix:
# Connect Server versions - tests exclude @pcc tags
# Simplified to reduce CI time: preview, release, one recent, one old
connect-version:
- "preview" # Nightly builds of Connect - catch regressions early
- "release" # Latest stable release + PCC cloud tests
- "2025.03.0" # Recent stable version
- "2023.03.0" # Oldest supported version
Comment on lines -35 to -41
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really want to stop running tests against old connect + preview?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure yet. Still exploring / experimenting on this branch - right now I'm just trying to wrap my head around the shape of the thing.


env:
# Debug mode input from workflow_dispatch or workflow_call
Expand Down Expand Up @@ -210,9 +205,6 @@ jobs:
# Clean up any static TOML files before tests
docker exec publisher-e2e.code-server rm -f /home/coder/workspace/static*.toml || true

# Run Cypress tests
# - For "release": run ALL tests (including @pcc cloud tests)
# - For other versions: exclude @pcc tests (cloud tests only run once on release)
- name: Run Cypress tests (${{ matrix.connect-version }})
id: run_tests
uses: posit-dev/with-connect@main
Expand All @@ -222,13 +214,7 @@ jobs:
port: 3939
command: |
cd test/e2e
if [ "${{ matrix.connect-version }}" = "release" ]; then
# Run all tests (including @pcc cloud tests)
CYPRESS_BOOTSTRAP_ADMIN_API_KEY=$CONNECT_API_KEY npx cypress run --headless --browser chrome
else
# Exclude @pcc cloud tests for non-release versions
CYPRESS_BOOTSTRAP_ADMIN_API_KEY=$CONNECT_API_KEY npx cypress run --headless --browser chrome --env grepFilterSpecs=true,grepOmitFiltered=true,grepTags=-@pcc
fi
CYPRESS_BOOTSTRAP_ADMIN_API_KEY=$CONNECT_API_KEY npx cypress run --headless --browser chrome
env:
CONNECT_CLOUD_ENV: staging
CI: true
Expand Down
279 changes: 278 additions & 1 deletion extensions/vscode/webviews/homeView/src/stores/home.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,100 @@ import { setActivePinia, createPinia } from "pinia";
import { beforeEach, describe, expect, test, vi } from "vitest";

import { useHomeStore } from "src/stores/home";
import type {
Configuration,
ConfigurationDetails,
ConfigurationError,
} from "../../../../src/api/types/configurations";
import { ContentType } from "../../../../src/api/types/configurations";
import { ProductType } from "../../../../src/api/types/contentRecords";
import type { PreContentRecord } from "../../../../src/api/types/contentRecords";
import type { Credential } from "../../../../src/api/types/credentials";
import {
ContentRecordState,
ServerType,
} from "../../../../src/api/types/contentRecords";
import type {
AgentErrorInvalidTOML,
AgentErrorTypeUnknown,
} from "../../../../src/api/types/error";
import type { ErrorCode } from "../../../../src/utils/errorTypes";

vi.mock(import("src/vscode"));
vi.mock("src/vscode", () => {
const postMessage = vi.fn();
const vscodeAPI = vi.fn(() => ({
postMessage: postMessage,
}));
return { vscodeAPI };
});

// Helper to create a minimal PreContentRecord for testing
function makeContentRecord(
overrides: Partial<PreContentRecord> = {},
): PreContentRecord {
return {
$schema: "",
serverType: ServerType.CONNECT,
serverUrl: "https://connect.example.com",
saveName: "test-deployment",
createdAt: "",
dismissedAt: "",
configurationName: "default",
type: ContentType.HTML,
deploymentError: null,
connectCloud: null,
deploymentName: "test-deployment",
deploymentPath: "/path/to/deployment.toml",
projectDir: ".",
state: ContentRecordState.NEW,
...overrides,
};
}

// Helper to create a valid Configuration
function makeConfiguration(
overrides: Omit<Partial<Configuration>, "configuration"> & {
configuration?: Partial<ConfigurationDetails>;
} = {},
): Configuration {
const { configuration: configOverrides, ...rest } = overrides;
return {
configurationName: "default",
configurationPath: "/path/to/config.toml",
configurationRelPath: ".posit/publish/default.toml",
projectDir: ".",
configuration: {
$schema: "",
productType: ProductType.CONNECT,
type: ContentType.HTML,
entrypoint: "index.html",
title: "My App",
files: ["/index.html"],
validate: true,
...configOverrides,
},
...rest,
};
}

// Helper to create a ConfigurationError
function makeConfigurationError(
overrides: Partial<ConfigurationError> = {},
): ConfigurationError {
return {
configurationName: "default",
configurationPath: "/path/to/config.toml",
configurationRelPath: ".posit/publish/default.toml",
projectDir: ".",
error: {
code: "unknown" as ErrorCode,
msg: "some error",
operation: "read",
data: {},
},
...overrides,
};
}

describe("Home Store", () => {
beforeEach(() => {
Expand All @@ -15,4 +107,189 @@ describe("Home Store", () => {
const home = useHomeStore();
expect(home.showDisabledOverlay).toBe(false);
});

describe("config.active error states", () => {
test("isMissing is true when config file is not found", () => {
const home = useHomeStore();

// Set up a content record that references a config name
const contentRecord = makeContentRecord({
configurationName: "deleted-config",
});
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;

// Don't add any matching configuration or error — simulates deleted file
expect(home.config.active.isMissing).toBe(true);
expect(home.config.active.isAlertActive).toBe(true);
});

test("isMissing is false when config exists", () => {
const home = useHomeStore();

const contentRecord = makeContentRecord({
configurationName: "default",
});
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;

// Add matching configuration
home.configurations.push(makeConfiguration());

expect(home.config.active.isMissing).toBe(false);
});

test("isTOMLError is true for invalidTOML config errors", () => {
const home = useHomeStore();

const contentRecord = makeContentRecord({
configurationName: "broken-config",
});
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;

// Add a configuration error with invalidTOML code
const error: AgentErrorInvalidTOML = {
code: "invalidTOML" as ErrorCode,
msg: "invalid TOML syntax",
operation: "read",
data: {
problem: "unexpected character",
file: "default.toml",
line: 5,
column: 10,
},
};
home.configurationsInError.push(
makeConfigurationError({
configurationName: "broken-config",
error,
}),
);

expect(home.config.active.isTOMLError).toBe(true);
expect(home.config.active.isAlertActive).toBe(true);
});

test("isTOMLError is true for unknownTOMLKey config errors", () => {
const home = useHomeStore();

const contentRecord = makeContentRecord({
configurationName: "broken-config",
});
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;

const error: AgentErrorInvalidTOML = {
code: "unknownTOMLKey" as ErrorCode,
msg: "invalidParam: not allowed",
operation: "read",
data: {
problem: "invalidParam: not allowed",
file: "default.toml",
line: 3,
column: 1,
},
};
home.configurationsInError.push(
makeConfigurationError({
configurationName: "broken-config",
error,
}),
);

expect(home.config.active.isTOMLError).toBe(true);
});

test("isUnknownError is true for non-TOML agent errors", () => {
const home = useHomeStore();

const contentRecord = makeContentRecord({
configurationName: "error-config",
});
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;

const error: AgentErrorTypeUnknown = {
code: "unknown" as ErrorCode,
msg: "something went wrong",
operation: "validate",
data: { detail: "unexpected" },
};
home.configurationsInError.push(
makeConfigurationError({
configurationName: "error-config",
error,
}),
);

expect(home.config.active.isUnknownError).toBe(true);
expect(home.config.active.isTOMLError).toBe(false);
expect(home.config.active.isAlertActive).toBe(true);
});

test("isUnknownType is true when config type is unknown", () => {
const home = useHomeStore();

const contentRecord = makeContentRecord({
configurationName: "unknown-type",
});
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;

home.configurations.push(
makeConfiguration({
configurationName: "unknown-type",
configuration: {
type: ContentType.UNKNOWN,
entrypoint: "unknown",
title: "Unknown",
},
}),
);

expect(home.config.active.isUnknownType).toBe(true);
});

test("isEntryMissing is true when content record has no config name", () => {
const home = useHomeStore();

const contentRecord = makeContentRecord({
configurationName: "",
});
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;

expect(home.config.active.isEntryMissing).toBe(true);
expect(home.config.active.isAlertActive).toBe(true);
});

test("no error states are active for a valid configuration", () => {
const home = useHomeStore();

const contentRecord = makeContentRecord();
home.contentRecords.push(contentRecord);
home.selectedContentRecord = contentRecord;
home.configurations.push(makeConfiguration());

// Add a credential so isCredentialMissing is also false
home.credentials.push({
guid: "",
name: "test-cred",
url: "https://connect.example.com",
apiKey: "",
accountId: "",
accountName: "",
refreshToken: "",
accessToken: "",
serverType: ServerType.CONNECT,
} as Credential);

expect(home.config.active.isMissing).toBe(false);
expect(home.config.active.isTOMLError).toBe(false);
expect(home.config.active.isUnknownError).toBe(false);
expect(home.config.active.isUnknownType).toBe(false);
expect(home.config.active.isEntryMissing).toBe(false);
});
});
});
Loading
Loading