From cf0d0bb0332b9d2c734ab2a2ce0b4869291cd916 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 21 Jun 2023 23:17:05 +0000 Subject: [PATCH 1/3] feat: add cohesive e2e tests for the web terminal, apps, and workspaces --- .gitignore | 1 + .prettierignore | 1 + cli/server.go | 85 +- cli/testdata/server-config.yaml.golden | 4 + coderd/apidoc/docs.go | 3 + coderd/apidoc/swagger.json | 3 + codersdk/deployment.go | 12 + docs/api/general.md | 1 + docs/api/schemas.md | 4 + examples/templates/docker/main.tf | 2 +- site/.eslintignore | 1 + site/.prettierignore | 1 + site/e2e/global.setup.ts | 18 + site/e2e/globalSetup.ts | 35 - site/e2e/helpers.ts | 240 ++++- site/e2e/playwright.config.ts | 33 +- site/e2e/provisionerGenerated.ts | 876 ++++++++++++++++++ site/e2e/tests/app.spec.ts | 52 ++ site/e2e/tests/createWorkspace.spec.ts | 19 + site/e2e/tests/listTemplates.spec.ts | 3 - site/e2e/tests/logout.spec.ts | 17 - site/e2e/tests/webTerminal.spec.ts | 40 + site/package.json | 7 +- site/src/api/typesGenerated.ts | 1 + site/src/components/FileUpload/FileUpload.tsx | 1 + site/src/components/FormFooter/FormFooter.tsx | 1 + site/src/components/Resources/AgentStatus.tsx | 1 + .../components/TerminalLink/TerminalLink.tsx | 1 + .../WorkspaceStatusBadge.tsx | 1 + site/src/pages/SetupPage/SetupPageView.tsx | 8 +- site/src/utils/tar.test.ts | 2 +- site/src/utils/tar.ts | 7 +- .../createTemplate/createTemplateXService.ts | 13 +- .../templateVersionEditorXService.ts | 2 +- site/yarn.lock | 171 +++- 35 files changed, 1512 insertions(+), 155 deletions(-) create mode 100644 site/e2e/global.setup.ts delete mode 100644 site/e2e/globalSetup.ts create mode 100644 site/e2e/provisionerGenerated.ts create mode 100644 site/e2e/tests/app.spec.ts create mode 100644 site/e2e/tests/createWorkspace.spec.ts delete mode 100644 site/e2e/tests/logout.spec.ts create mode 100644 site/e2e/tests/webTerminal.spec.ts diff --git a/.gitignore b/.gitignore index 173b72ae41b38..b22db03c2089e 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ site/storybook-static/ site/test-results/* site/e2e/test-results/* site/e2e/states/*.json +site/e2e/.auth.json site/playwright-report/* site/.swc site/dist/ diff --git a/.prettierignore b/.prettierignore index 866c9b3eb889a..fe39bea750359 100644 --- a/.prettierignore +++ b/.prettierignore @@ -30,6 +30,7 @@ site/storybook-static/ site/test-results/* site/e2e/test-results/* site/e2e/states/*.json +site/e2e/.auth.json site/playwright-report/* site/.swc site/dist/ diff --git a/cli/server.go b/cli/server.go index e30bf5fd71fde..b918b0405bcbd 100644 --- a/cli/server.go +++ b/cli/server.go @@ -820,7 +820,7 @@ func (r *RootCmd) Server(newAPI func(context.Context, *coderd.Options) (*coderd. for i := int64(0); i < cfg.Provisioner.Daemons.Value(); i++ { daemonCacheDir := filepath.Join(cacheDir, fmt.Sprintf("provisioner-%d", i)) daemon, err := newProvisionerDaemon( - ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, false, &provisionerdWaitGroup, + ctx, coderAPI, provisionerdMetrics, logger, cfg, daemonCacheDir, errCh, &provisionerdWaitGroup, ) if err != nil { return xerrors.Errorf("create provisioner daemon: %w", err) @@ -1176,7 +1176,6 @@ func newProvisionerDaemon( cfg *codersdk.DeploymentValues, cacheDir string, errCh chan error, - dev bool, wg *sync.WaitGroup, ) (srv *provisionerd.Server, err error) { ctx, cancel := context.WithCancel(ctx) @@ -1191,53 +1190,14 @@ func newProvisionerDaemon( return nil, xerrors.Errorf("mkdir %q: %w", cacheDir, err) } - tfDir := filepath.Join(cacheDir, "tf") - err = os.MkdirAll(tfDir, 0o700) - if err != nil { - return nil, xerrors.Errorf("mkdir terraform dir: %w", err) - } - - tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName) - terraformClient, terraformServer := provisionersdk.MemTransportPipe() - wg.Add(1) - go func() { - defer wg.Done() - <-ctx.Done() - _ = terraformClient.Close() - _ = terraformServer.Close() - }() - wg.Add(1) - go func() { - defer wg.Done() - defer cancel() - - err := terraform.Serve(ctx, &terraform.ServeOptions{ - ServeOptions: &provisionersdk.ServeOptions{ - Listener: terraformServer, - }, - CachePath: tfDir, - Logger: logger, - Tracer: tracer, - }) - if err != nil && !xerrors.Is(err, context.Canceled) { - select { - case errCh <- err: - default: - } - } - }() - workDir := filepath.Join(cacheDir, "work") err = os.MkdirAll(workDir, 0o700) if err != nil { return nil, xerrors.Errorf("mkdir work dir: %w", err) } - provisioners := provisionerd.Provisioners{ - string(database.ProvisionerTypeTerraform): sdkproto.NewDRPCProvisionerClient(terraformClient), - } - // include echo provisioner when in dev mode - if dev { + provisioners := provisionerd.Provisioners{} + if cfg.Provisioner.DaemonsEcho { echoClient, echoServer := provisionersdk.MemTransportPipe() wg.Add(1) go func() { @@ -1260,7 +1220,46 @@ func newProvisionerDaemon( } }() provisioners[string(database.ProvisionerTypeEcho)] = sdkproto.NewDRPCProvisionerClient(echoClient) + } else { + tfDir := filepath.Join(cacheDir, "tf") + err = os.MkdirAll(tfDir, 0o700) + if err != nil { + return nil, xerrors.Errorf("mkdir terraform dir: %w", err) + } + + tracer := coderAPI.TracerProvider.Tracer(tracing.TracerName) + terraformClient, terraformServer := provisionersdk.MemTransportPipe() + wg.Add(1) + go func() { + defer wg.Done() + <-ctx.Done() + _ = terraformClient.Close() + _ = terraformServer.Close() + }() + wg.Add(1) + go func() { + defer wg.Done() + defer cancel() + + err := terraform.Serve(ctx, &terraform.ServeOptions{ + ServeOptions: &provisionersdk.ServeOptions{ + Listener: terraformServer, + }, + CachePath: tfDir, + Logger: logger, + Tracer: tracer, + }) + if err != nil && !xerrors.Is(err, context.Canceled) { + select { + case errCh <- err: + default: + } + } + }() + + provisioners[string(database.ProvisionerTypeTerraform)] = sdkproto.NewDRPCProvisionerClient(terraformClient) } + debounce := time.Second return provisionerd.New(func(ctx context.Context) (proto.DRPCProvisionerDaemonClient, error) { // This debounces calls to listen every second. Read the comment diff --git a/cli/testdata/server-config.yaml.golden b/cli/testdata/server-config.yaml.golden index 79a678e7abd2f..3eeb054f5e4e3 100644 --- a/cli/testdata/server-config.yaml.golden +++ b/cli/testdata/server-config.yaml.golden @@ -281,6 +281,10 @@ provisioning: # state for a long time, consider increasing this. # (default: 3, type: int) daemons: 3 + # Whether to use echo provisioner daemons instead of Terraform. This is for E2E + # tests. + # (default: false, type: bool) + daemonsEcho: false # Time to wait before polling for a new job. # (default: 1s, type: duration) daemonPollInterval: 1s diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 8fc32811518c7..a84f41173efe2 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -7911,6 +7911,9 @@ const docTemplate = `{ "daemons": { "type": "integer" }, + "daemons_echo": { + "type": "boolean" + }, "force_cancel_interval": { "type": "integer" } diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index df0524bad7e78..30d70f51171ae 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -7090,6 +7090,9 @@ "daemons": { "type": "integer" }, + "daemons_echo": { + "type": "boolean" + }, "force_cancel_interval": { "type": "integer" } diff --git a/codersdk/deployment.go b/codersdk/deployment.go index aa62aebba5cdd..27a853ec4756e 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -309,6 +309,7 @@ type GitAuthConfig struct { type ProvisionerConfig struct { Daemons clibase.Int64 `json:"daemons" typescript:",notnull"` + DaemonsEcho clibase.Bool `json:"daemons_echo" typescript:",notnull"` DaemonPollInterval clibase.Duration `json:"daemon_poll_interval" typescript:",notnull"` DaemonPollJitter clibase.Duration `json:"daemon_poll_jitter" typescript:",notnull"` ForceCancelInterval clibase.Duration `json:"force_cancel_interval" typescript:",notnull"` @@ -1080,6 +1081,17 @@ when required by your organization's security policy.`, Group: &deploymentGroupProvisioning, YAML: "daemons", }, + { + Name: "Echo Provisioner", + Description: "Whether to use echo provisioner daemons instead of Terraform. This is for E2E tests.", + Flag: "provisioner-daemons-echo", + Env: "CODER_PROVISIONER_DAEMONS_ECHO", + Hidden: true, + Default: "false", + Value: &c.Provisioner.DaemonsEcho, + Group: &deploymentGroupProvisioning, + YAML: "daemonsEcho", + }, { Name: "Poll Interval", Description: "Time to wait before polling for a new job.", diff --git a/docs/api/general.md b/docs/api/general.md index 22be046b47e64..e4ed489ee63bb 100644 --- a/docs/api/general.md +++ b/docs/api/general.md @@ -282,6 +282,7 @@ curl -X GET http://coder-server:8080/api/v2/deployment/config \ "daemon_poll_interval": 0, "daemon_poll_jitter": 0, "daemons": 0, + "daemons_echo": true, "force_cancel_interval": 0 }, "proxy_health_status_interval": 0, diff --git a/docs/api/schemas.md b/docs/api/schemas.md index caac33caea53b..5c0f71439ffce 100644 --- a/docs/api/schemas.md +++ b/docs/api/schemas.md @@ -1954,6 +1954,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "daemon_poll_interval": 0, "daemon_poll_jitter": 0, "daemons": 0, + "daemons_echo": true, "force_cancel_interval": 0 }, "proxy_health_status_interval": 0, @@ -2282,6 +2283,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "daemon_poll_interval": 0, "daemon_poll_jitter": 0, "daemons": 0, + "daemons_echo": true, "force_cancel_interval": 0 }, "proxy_health_status_interval": 0, @@ -3118,6 +3120,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in "daemon_poll_interval": 0, "daemon_poll_jitter": 0, "daemons": 0, + "daemons_echo": true, "force_cancel_interval": 0 } ``` @@ -3129,6 +3132,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in | `daemon_poll_interval` | integer | false | | | | `daemon_poll_jitter` | integer | false | | | | `daemons` | integer | false | | | +| `daemons_echo` | boolean | false | | | | `force_cancel_interval` | integer | false | | | ## codersdk.ProvisionerDaemon diff --git a/examples/templates/docker/main.tf b/examples/templates/docker/main.tf index d30aa8c1f8afa..7749cd983b835 100644 --- a/examples/templates/docker/main.tf +++ b/examples/templates/docker/main.tf @@ -2,7 +2,7 @@ terraform { required_providers { coder = { source = "coder/coder" - version = "~> 0.7.0" + version = "~> 0.8.3" } docker = { source = "kreuzwerker/docker" diff --git a/site/.eslintignore b/site/.eslintignore index 9202d0bf186b0..359b7c3e0eea2 100644 --- a/site/.eslintignore +++ b/site/.eslintignore @@ -30,6 +30,7 @@ storybook-static/ test-results/* e2e/test-results/* e2e/states/*.json +e2e/.auth.json playwright-report/* .swc dist/ diff --git a/site/.prettierignore b/site/.prettierignore index 9202d0bf186b0..359b7c3e0eea2 100644 --- a/site/.prettierignore +++ b/site/.prettierignore @@ -30,6 +30,7 @@ storybook-static/ test-results/* e2e/test-results/* e2e/states/*.json +e2e/.auth.json playwright-report/* .swc dist/ diff --git a/site/e2e/global.setup.ts b/site/e2e/global.setup.ts new file mode 100644 index 0000000000000..1da306c5c36cd --- /dev/null +++ b/site/e2e/global.setup.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test" +import * as constants from "./constants" +import { STORAGE_STATE } from "./playwright.config" +import { Language } from "../src/components/CreateUserForm/CreateUserForm" + +test("create first user", async ({ page }) => { + await page.goto("/", { waitUntil: "networkidle" }) + + await page.getByLabel(Language.usernameLabel).fill(constants.username) + await page.getByLabel(Language.emailLabel).fill(constants.email) + await page.getByLabel(Language.passwordLabel).fill(constants.password) + await page.getByTestId("trial").click() + await page.getByTestId("create").click() + + await expect(page).toHaveURL("/workspaces") + + await page.context().storageState({ path: STORAGE_STATE }) +}) diff --git a/site/e2e/globalSetup.ts b/site/e2e/globalSetup.ts deleted file mode 100644 index ad61cca6f0f3d..0000000000000 --- a/site/e2e/globalSetup.ts +++ /dev/null @@ -1,35 +0,0 @@ -import axios from "axios" -import { request } from "playwright" -import { createFirstUser } from "../src/api/api" -import * as constants from "./constants" -import { getStatePath } from "./helpers" - -const globalSetup = async (): Promise => { - axios.defaults.baseURL = `http://localhost:${constants.defaultPort}` - - // Create first user - await createFirstUser({ - email: constants.email, - username: constants.username, - password: constants.password, - trial: false, - }) - - // Authenticated storage - const authenticatedRequestContext = await request.newContext() - await authenticatedRequestContext.post( - `http://localhost:${constants.defaultPort}/api/v2/users/login`, - { - data: { - email: constants.email, - password: constants.password, - }, - }, - ) - await authenticatedRequestContext.storageState({ - path: getStatePath("authState"), - }) - await authenticatedRequestContext.dispose() -} - -export default globalSetup diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 1b8defa88c4e2..00adb555c9c36 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -1,31 +1,229 @@ -import { Page } from "@playwright/test" +import { expect, Page } from "@playwright/test" +import { spawn } from "child_process" +import { randomUUID } from "crypto" import path from "path" +import { TarWriter } from "utils/tar" +import { + Agent, + App, + AppSharingLevel, + Parse_Complete, + Parse_Response, + Provision_Complete, + Provision_Response, + Resource, +} from "./provisionerGenerated" +import { port } from "./playwright.config" -export const buttons = { - starterTemplates: "Starter templates", - dockerTemplate: "Develop in Docker", - useTemplate: "Use template", - createTemplate: "Create template", - createWorkspace: "Create workspace", - submitCreateWorkspace: "Create workspace", - stopWorkspace: "Stop", - startWorkspace: "Start", -} +// createWorkspace creates a workspace for a template. +// It does not wait for it to be running, but it does navigate to the page. +export const createWorkspace = async ( + page: Page, + templateName: string, +): Promise => { + await page.goto("/templates/" + templateName + "/workspace", { + waitUntil: "networkidle", + }) + const name = randomName() + await page.getByLabel("name").fill(name) + await page.getByTestId("form-submit").click() -export const clickButton = async (page: Page, name: string): Promise => { - await page.getByRole("button", { name, exact: true }).click() + await expect(page).toHaveURL("/@admin/" + name) + await page.getByTestId("build-status").isVisible() + return name } -export const fillInput = async ( +// createTemplate navigates to the /templates/new page and uploads a template +// with the resources provided in the responses argument. +export const createTemplate = async ( page: Page, - label: string, - value: string, -): Promise => { - await page.fill(`text=${label}`, value) + responses?: EchoProvisionerResponses, +): Promise => { + // Required to have templates submit their provisioner type as echo! + await page.addInitScript({ + content: "window.playwright = true", + }) + await page.goto("/templates/new", { waitUntil: "networkidle" }) + await page.getByTestId("file-upload").setInputFiles({ + buffer: await createTemplateVersionTar(responses), + mimeType: "application/x-tar", + name: "template.tar", + }) + const name = randomName() + await page.getByLabel("Name *").fill(name) + await page.getByTestId("form-submit").click() + await expect(page).toHaveURL("/templates/" + name, { + timeout: 30000, + }) + return name +} + +// startAgent runs the coder agent with the provided token. +// It awaits the agent to be ready before returning. +export const startAgent = async (page: Page, token: string): Promise => { + const coderMain = path.join( + __dirname, + "..", + "..", + "enterprise", + "cmd", + "coder", + "main.go", + ) + const cp = spawn("go", ["run", coderMain, "agent", "--no-reap"], { + env: { + ...process.env, + CODER_AGENT_URL: "http://localhost:"+port, + CODER_AGENT_TOKEN: token, + }, + }) + let buffer = Buffer.of() + cp.stderr.on("data", (data: Buffer) => { + buffer = Buffer.concat([buffer, data]) + }) + try { + await page.getByTestId("agent-status-ready").isVisible() + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- The error is a string + } catch (ex: any) { + throw new Error(ex.toString() + "\n" + buffer.toString()) + } +} + +// Allows users to more easily define properties they want for agents and resources! +type RecursivePartial = { + [P in keyof T]?: T[P] extends (infer U)[] + ? RecursivePartial[] + : T[P] extends object | undefined + ? RecursivePartial + : T[P] +} + +interface EchoProvisionerResponses { + // parse is for observing any Terraform variables + parse?: RecursivePartial[] + // plan occurs when the template is imported + plan?: RecursivePartial[] + // apply occurs when the workspace is built + apply?: RecursivePartial[] } -const statesDir = path.join(__dirname, "./states") +// createTemplateVersionTar consumes a series of echo provisioner protobufs and +// converts it into an uploadable tar file. +const createTemplateVersionTar = async ( + responses?: EchoProvisionerResponses, +): Promise => { + if (!responses) { + responses = {} + } + if (!responses.parse) { + responses.parse = [{}] + } + if (!responses.apply) { + responses.apply = [{}] + } + if (!responses.plan) { + responses.plan = responses.apply + } + + const tar = new TarWriter() + responses.parse.forEach((response, index) => { + response.complete = { + templateVariables: [], + ...response.complete, + } as Parse_Complete + tar.addFile( + `${index}.parse.protobuf`, + Parse_Response.encode(response as Parse_Response).finish(), + ) + }) + + const fillProvisionResponse = ( + response: RecursivePartial, + ) => { + response.complete = { + error: "", + state: new Uint8Array(), + resources: [], + parameters: [], + gitAuthProviders: [], + plan: new Uint8Array(), + ...response.complete, + } as Provision_Complete + response.complete.resources = response.complete.resources?.map( + (resource) => { + if (resource.agents) { + resource.agents = resource.agents?.map((agent) => { + if (agent.apps) { + agent.apps = agent.apps?.map((app) => { + return { + command: "", + displayName: "example", + external: false, + icon: "", + sharingLevel: AppSharingLevel.PUBLIC, + slug: "example", + subdomain: false, + url: "", + ...app, + } as App + }) + } + return { + apps: [], + architecture: "amd64", + connectionTimeoutSeconds: 300, + directory: "", + env: {}, + id: randomUUID(), + metadata: [], + motdFile: "", + name: "dev", + operatingSystem: "linux", + shutdownScript: "", + shutdownScriptTimeoutSeconds: 0, + startupScript: "", + startupScriptBehavior: "", + startupScriptTimeoutSeconds: 300, + troubleshootingUrl: "", + token: randomUUID(), + ...agent, + } as Agent + }) + } + return { + agents: [], + dailyCost: 0, + hide: false, + icon: "", + instanceType: "", + metadata: [], + name: "dev", + type: "echo", + ...resource, + } as Resource + }, + ) + } + + responses.apply.forEach((response, index) => { + fillProvisionResponse(response) + + tar.addFile( + `${index}.provision.apply.protobuf`, + Provision_Response.encode(response as Provision_Response).finish(), + ) + }) + responses.plan.forEach((response, index) => { + fillProvisionResponse(response) + + tar.addFile( + `${index}.provision.plan.protobuf`, + Provision_Response.encode(response as Provision_Response).finish(), + ) + }) + return Buffer.from((await tar.write()) as ArrayBuffer) +} -export const getStatePath = (name: string): string => { - return path.join(statesDir, `${name}.json`) +const randomName = () => { + return randomUUID().slice(0, 8) } diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index 43c01871dddc0..a0cfcb7f197f5 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -1,25 +1,44 @@ -import { PlaywrightTestConfig } from "@playwright/test" +import { defineConfig } from "@playwright/test" import path from "path" import { defaultPort } from "./constants" -const port = process.env.CODER_E2E_PORT +export const port = process.env.CODER_E2E_PORT ? Number(process.env.CODER_E2E_PORT) : defaultPort const coderMain = path.join(__dirname, "../../enterprise/cmd/coder/main.go") -const config: PlaywrightTestConfig = { - testDir: "tests", - globalSetup: require.resolve("./globalSetup"), +export const STORAGE_STATE = path.join(__dirname, ".auth.json") + +const config = defineConfig({ + projects: [ + { + name: "setup", + testMatch: /global.setup\.ts/, + }, + { + name: "tests", + testMatch: /.*\.spec\.ts/, + dependencies: ["setup"], + use: { + storageState: STORAGE_STATE, + }, + }, + ], use: { baseURL: `http://localhost:${port}`, video: "retain-on-failure", }, webServer: { - command: `go run -tags embed ${coderMain} server --global-config $(mktemp -d -t e2e-XXXXXXXXXX)`, + command: + `go run -tags embed ${coderMain} server --global-config ` + + `$(mktemp -d -t e2e-XXXXXXXXXX) --in-memory --telemetry=false ` + + `--provisioner-daemons 10 ` + + `--provisioner-daemons-echo ` + + `--provisioner-daemon-poll-interval 50ms`, port, reuseExistingServer: false, }, -} +}) export default config diff --git a/site/e2e/provisionerGenerated.ts b/site/e2e/provisionerGenerated.ts new file mode 100644 index 0000000000000..9cb96dbfdc370 --- /dev/null +++ b/site/e2e/provisionerGenerated.ts @@ -0,0 +1,876 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal" +import { Observable } from "rxjs" + +export const protobufPackage = "provisioner" + +/** LogLevel represents severity of the log. */ +export enum LogLevel { + TRACE = 0, + DEBUG = 1, + INFO = 2, + WARN = 3, + ERROR = 4, + UNRECOGNIZED = -1, +} + +export enum AppSharingLevel { + OWNER = 0, + AUTHENTICATED = 1, + PUBLIC = 2, + UNRECOGNIZED = -1, +} + +export enum WorkspaceTransition { + START = 0, + STOP = 1, + DESTROY = 2, + UNRECOGNIZED = -1, +} + +/** Empty indicates a successful request/response. */ +export interface Empty {} + +/** TemplateVariable represents a Terraform variable. */ +export interface TemplateVariable { + name: string + description: string + type: string + defaultValue: string + required: boolean + sensitive: boolean +} + +/** RichParameterOption represents a singular option that a parameter may expose. */ +export interface RichParameterOption { + name: string + description: string + value: string + icon: string +} + +/** RichParameter represents a variable that is exposed. */ +export interface RichParameter { + name: string + description: string + type: string + mutable: boolean + defaultValue: string + icon: string + options: RichParameterOption[] + validationRegex: string + validationError: string + validationMin?: number | undefined + validationMax?: number | undefined + validationMonotonic: string + required: boolean + legacyVariableName: string + displayName: string +} + +/** RichParameterValue holds the key/value mapping of a parameter. */ +export interface RichParameterValue { + name: string + value: string +} + +/** VariableValue holds the key/value mapping of a Terraform variable. */ +export interface VariableValue { + name: string + value: string + sensitive: boolean +} + +/** Log represents output from a request. */ +export interface Log { + level: LogLevel + output: string +} + +export interface InstanceIdentityAuth { + instanceId: string +} + +export interface GitAuthProvider { + id: string + accessToken: string +} + +/** Agent represents a running agent on the workspace. */ +export interface Agent { + id: string + name: string + env: { [key: string]: string } + startupScript: string + operatingSystem: string + architecture: string + directory: string + apps: App[] + token?: string | undefined + instanceId?: string | undefined + connectionTimeoutSeconds: number + troubleshootingUrl: string + motdFile: string + /** Field 14 was bool login_before_ready = 14, now removed. */ + startupScriptTimeoutSeconds: number + shutdownScript: string + shutdownScriptTimeoutSeconds: number + metadata: Agent_Metadata[] + startupScriptBehavior: string +} + +export interface Agent_Metadata { + key: string + displayName: string + script: string + interval: number + timeout: number +} + +export interface Agent_EnvEntry { + key: string + value: string +} + +/** App represents a dev-accessible application on the workspace. */ +export interface App { + /** + * slug is the unique identifier for the app, usually the name from the + * template. It must be URL-safe and hostname-safe. + */ + slug: string + displayName: string + command: string + url: string + icon: string + subdomain: boolean + healthcheck: Healthcheck | undefined + sharingLevel: AppSharingLevel + external: boolean +} + +/** Healthcheck represents configuration for checking for app readiness. */ +export interface Healthcheck { + url: string + interval: number + threshold: number +} + +/** Resource represents created infrastructure. */ +export interface Resource { + name: string + type: string + agents: Agent[] + metadata: Resource_Metadata[] + hide: boolean + icon: string + instanceType: string + dailyCost: number +} + +export interface Resource_Metadata { + key: string + value: string + sensitive: boolean + isNull: boolean +} + +/** Parse consumes source-code from a directory to produce inputs. */ +export interface Parse {} + +export interface Parse_Request { + directory: string +} + +export interface Parse_Complete { + templateVariables: TemplateVariable[] +} + +export interface Parse_Response { + log?: Log | undefined + complete?: Parse_Complete | undefined +} + +/** + * Provision consumes source-code from a directory to produce resources. + * Exactly one of Plan or Apply must be provided in a single session. + */ +export interface Provision {} + +export interface Provision_Metadata { + coderUrl: string + workspaceTransition: WorkspaceTransition + workspaceName: string + workspaceOwner: string + workspaceId: string + workspaceOwnerId: string + workspaceOwnerEmail: string + templateName: string + templateVersion: string + workspaceOwnerOidcAccessToken: string + workspaceOwnerSessionToken: string +} + +/** + * Config represents execution configuration shared by both Plan and + * Apply commands. + */ +export interface Provision_Config { + directory: string + state: Uint8Array + metadata: Provision_Metadata | undefined + provisionerLogLevel: string +} + +export interface Provision_Plan { + config: Provision_Config | undefined + richParameterValues: RichParameterValue[] + variableValues: VariableValue[] + gitAuthProviders: GitAuthProvider[] +} + +export interface Provision_Apply { + config: Provision_Config | undefined + plan: Uint8Array +} + +export interface Provision_Cancel {} + +export interface Provision_Request { + plan?: Provision_Plan | undefined + apply?: Provision_Apply | undefined + cancel?: Provision_Cancel | undefined +} + +export interface Provision_Complete { + state: Uint8Array + error: string + resources: Resource[] + parameters: RichParameter[] + gitAuthProviders: string[] + plan: Uint8Array +} + +export interface Provision_Response { + log?: Log | undefined + complete?: Provision_Complete | undefined +} + +export const Empty = { + encode(_: Empty, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer + }, +} + +export const TemplateVariable = { + encode( + message: TemplateVariable, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name) + } + if (message.description !== "") { + writer.uint32(18).string(message.description) + } + if (message.type !== "") { + writer.uint32(26).string(message.type) + } + if (message.defaultValue !== "") { + writer.uint32(34).string(message.defaultValue) + } + if (message.required === true) { + writer.uint32(40).bool(message.required) + } + if (message.sensitive === true) { + writer.uint32(48).bool(message.sensitive) + } + return writer + }, +} + +export const RichParameterOption = { + encode( + message: RichParameterOption, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name) + } + if (message.description !== "") { + writer.uint32(18).string(message.description) + } + if (message.value !== "") { + writer.uint32(26).string(message.value) + } + if (message.icon !== "") { + writer.uint32(34).string(message.icon) + } + return writer + }, +} + +export const RichParameter = { + encode( + message: RichParameter, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name) + } + if (message.description !== "") { + writer.uint32(18).string(message.description) + } + if (message.type !== "") { + writer.uint32(26).string(message.type) + } + if (message.mutable === true) { + writer.uint32(32).bool(message.mutable) + } + if (message.defaultValue !== "") { + writer.uint32(42).string(message.defaultValue) + } + if (message.icon !== "") { + writer.uint32(50).string(message.icon) + } + for (const v of message.options) { + RichParameterOption.encode(v!, writer.uint32(58).fork()).ldelim() + } + if (message.validationRegex !== "") { + writer.uint32(66).string(message.validationRegex) + } + if (message.validationError !== "") { + writer.uint32(74).string(message.validationError) + } + if (message.validationMin !== undefined) { + writer.uint32(80).int32(message.validationMin) + } + if (message.validationMax !== undefined) { + writer.uint32(88).int32(message.validationMax) + } + if (message.validationMonotonic !== "") { + writer.uint32(98).string(message.validationMonotonic) + } + if (message.required === true) { + writer.uint32(104).bool(message.required) + } + if (message.legacyVariableName !== "") { + writer.uint32(114).string(message.legacyVariableName) + } + if (message.displayName !== "") { + writer.uint32(122).string(message.displayName) + } + return writer + }, +} + +export const RichParameterValue = { + encode( + message: RichParameterValue, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name) + } + if (message.value !== "") { + writer.uint32(18).string(message.value) + } + return writer + }, +} + +export const VariableValue = { + encode( + message: VariableValue, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name) + } + if (message.value !== "") { + writer.uint32(18).string(message.value) + } + if (message.sensitive === true) { + writer.uint32(24).bool(message.sensitive) + } + return writer + }, +} + +export const Log = { + encode(message: Log, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.level !== 0) { + writer.uint32(8).int32(message.level) + } + if (message.output !== "") { + writer.uint32(18).string(message.output) + } + return writer + }, +} + +export const InstanceIdentityAuth = { + encode( + message: InstanceIdentityAuth, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.instanceId !== "") { + writer.uint32(10).string(message.instanceId) + } + return writer + }, +} + +export const GitAuthProvider = { + encode( + message: GitAuthProvider, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.id !== "") { + writer.uint32(10).string(message.id) + } + if (message.accessToken !== "") { + writer.uint32(18).string(message.accessToken) + } + return writer + }, +} + +export const Agent = { + encode(message: Agent, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.id !== "") { + writer.uint32(10).string(message.id) + } + if (message.name !== "") { + writer.uint32(18).string(message.name) + } + Object.entries(message.env).forEach(([key, value]) => { + Agent_EnvEntry.encode( + { key: key as any, value }, + writer.uint32(26).fork(), + ).ldelim() + }) + if (message.startupScript !== "") { + writer.uint32(34).string(message.startupScript) + } + if (message.operatingSystem !== "") { + writer.uint32(42).string(message.operatingSystem) + } + if (message.architecture !== "") { + writer.uint32(50).string(message.architecture) + } + if (message.directory !== "") { + writer.uint32(58).string(message.directory) + } + for (const v of message.apps) { + App.encode(v!, writer.uint32(66).fork()).ldelim() + } + if (message.token !== undefined) { + writer.uint32(74).string(message.token) + } + if (message.instanceId !== undefined) { + writer.uint32(82).string(message.instanceId) + } + if (message.connectionTimeoutSeconds !== 0) { + writer.uint32(88).int32(message.connectionTimeoutSeconds) + } + if (message.troubleshootingUrl !== "") { + writer.uint32(98).string(message.troubleshootingUrl) + } + if (message.motdFile !== "") { + writer.uint32(106).string(message.motdFile) + } + if (message.startupScriptTimeoutSeconds !== 0) { + writer.uint32(120).int32(message.startupScriptTimeoutSeconds) + } + if (message.shutdownScript !== "") { + writer.uint32(130).string(message.shutdownScript) + } + if (message.shutdownScriptTimeoutSeconds !== 0) { + writer.uint32(136).int32(message.shutdownScriptTimeoutSeconds) + } + for (const v of message.metadata) { + Agent_Metadata.encode(v!, writer.uint32(146).fork()).ldelim() + } + if (message.startupScriptBehavior !== "") { + writer.uint32(154).string(message.startupScriptBehavior) + } + return writer + }, +} + +export const Agent_Metadata = { + encode( + message: Agent_Metadata, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.key !== "") { + writer.uint32(10).string(message.key) + } + if (message.displayName !== "") { + writer.uint32(18).string(message.displayName) + } + if (message.script !== "") { + writer.uint32(26).string(message.script) + } + if (message.interval !== 0) { + writer.uint32(32).int64(message.interval) + } + if (message.timeout !== 0) { + writer.uint32(40).int64(message.timeout) + } + return writer + }, +} + +export const Agent_EnvEntry = { + encode( + message: Agent_EnvEntry, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.key !== "") { + writer.uint32(10).string(message.key) + } + if (message.value !== "") { + writer.uint32(18).string(message.value) + } + return writer + }, +} + +export const App = { + encode(message: App, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.slug !== "") { + writer.uint32(10).string(message.slug) + } + if (message.displayName !== "") { + writer.uint32(18).string(message.displayName) + } + if (message.command !== "") { + writer.uint32(26).string(message.command) + } + if (message.url !== "") { + writer.uint32(34).string(message.url) + } + if (message.icon !== "") { + writer.uint32(42).string(message.icon) + } + if (message.subdomain === true) { + writer.uint32(48).bool(message.subdomain) + } + if (message.healthcheck !== undefined) { + Healthcheck.encode(message.healthcheck, writer.uint32(58).fork()).ldelim() + } + if (message.sharingLevel !== 0) { + writer.uint32(64).int32(message.sharingLevel) + } + if (message.external === true) { + writer.uint32(72).bool(message.external) + } + return writer + }, +} + +export const Healthcheck = { + encode( + message: Healthcheck, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.url !== "") { + writer.uint32(10).string(message.url) + } + if (message.interval !== 0) { + writer.uint32(16).int32(message.interval) + } + if (message.threshold !== 0) { + writer.uint32(24).int32(message.threshold) + } + return writer + }, +} + +export const Resource = { + encode( + message: Resource, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name) + } + if (message.type !== "") { + writer.uint32(18).string(message.type) + } + for (const v of message.agents) { + Agent.encode(v!, writer.uint32(26).fork()).ldelim() + } + for (const v of message.metadata) { + Resource_Metadata.encode(v!, writer.uint32(34).fork()).ldelim() + } + if (message.hide === true) { + writer.uint32(40).bool(message.hide) + } + if (message.icon !== "") { + writer.uint32(50).string(message.icon) + } + if (message.instanceType !== "") { + writer.uint32(58).string(message.instanceType) + } + if (message.dailyCost !== 0) { + writer.uint32(64).int32(message.dailyCost) + } + return writer + }, +} + +export const Resource_Metadata = { + encode( + message: Resource_Metadata, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.key !== "") { + writer.uint32(10).string(message.key) + } + if (message.value !== "") { + writer.uint32(18).string(message.value) + } + if (message.sensitive === true) { + writer.uint32(24).bool(message.sensitive) + } + if (message.isNull === true) { + writer.uint32(32).bool(message.isNull) + } + return writer + }, +} + +export const Parse = { + encode(_: Parse, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer + }, +} + +export const Parse_Request = { + encode( + message: Parse_Request, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.directory !== "") { + writer.uint32(10).string(message.directory) + } + return writer + }, +} + +export const Parse_Complete = { + encode( + message: Parse_Complete, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + for (const v of message.templateVariables) { + TemplateVariable.encode(v!, writer.uint32(10).fork()).ldelim() + } + return writer + }, +} + +export const Parse_Response = { + encode( + message: Parse_Response, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.log !== undefined) { + Log.encode(message.log, writer.uint32(10).fork()).ldelim() + } + if (message.complete !== undefined) { + Parse_Complete.encode(message.complete, writer.uint32(18).fork()).ldelim() + } + return writer + }, +} + +export const Provision = { + encode(_: Provision, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + return writer + }, +} + +export const Provision_Metadata = { + encode( + message: Provision_Metadata, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.coderUrl !== "") { + writer.uint32(10).string(message.coderUrl) + } + if (message.workspaceTransition !== 0) { + writer.uint32(16).int32(message.workspaceTransition) + } + if (message.workspaceName !== "") { + writer.uint32(26).string(message.workspaceName) + } + if (message.workspaceOwner !== "") { + writer.uint32(34).string(message.workspaceOwner) + } + if (message.workspaceId !== "") { + writer.uint32(42).string(message.workspaceId) + } + if (message.workspaceOwnerId !== "") { + writer.uint32(50).string(message.workspaceOwnerId) + } + if (message.workspaceOwnerEmail !== "") { + writer.uint32(58).string(message.workspaceOwnerEmail) + } + if (message.templateName !== "") { + writer.uint32(66).string(message.templateName) + } + if (message.templateVersion !== "") { + writer.uint32(74).string(message.templateVersion) + } + if (message.workspaceOwnerOidcAccessToken !== "") { + writer.uint32(82).string(message.workspaceOwnerOidcAccessToken) + } + if (message.workspaceOwnerSessionToken !== "") { + writer.uint32(90).string(message.workspaceOwnerSessionToken) + } + return writer + }, +} + +export const Provision_Config = { + encode( + message: Provision_Config, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.directory !== "") { + writer.uint32(10).string(message.directory) + } + if (message.state.length !== 0) { + writer.uint32(18).bytes(message.state) + } + if (message.metadata !== undefined) { + Provision_Metadata.encode( + message.metadata, + writer.uint32(26).fork(), + ).ldelim() + } + if (message.provisionerLogLevel !== "") { + writer.uint32(34).string(message.provisionerLogLevel) + } + return writer + }, +} + +export const Provision_Plan = { + encode( + message: Provision_Plan, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.config !== undefined) { + Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim() + } + for (const v of message.richParameterValues) { + RichParameterValue.encode(v!, writer.uint32(26).fork()).ldelim() + } + for (const v of message.variableValues) { + VariableValue.encode(v!, writer.uint32(34).fork()).ldelim() + } + for (const v of message.gitAuthProviders) { + GitAuthProvider.encode(v!, writer.uint32(42).fork()).ldelim() + } + return writer + }, +} + +export const Provision_Apply = { + encode( + message: Provision_Apply, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.config !== undefined) { + Provision_Config.encode(message.config, writer.uint32(10).fork()).ldelim() + } + if (message.plan.length !== 0) { + writer.uint32(18).bytes(message.plan) + } + return writer + }, +} + +export const Provision_Cancel = { + encode( + _: Provision_Cancel, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + return writer + }, +} + +export const Provision_Request = { + encode( + message: Provision_Request, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.plan !== undefined) { + Provision_Plan.encode(message.plan, writer.uint32(10).fork()).ldelim() + } + if (message.apply !== undefined) { + Provision_Apply.encode(message.apply, writer.uint32(18).fork()).ldelim() + } + if (message.cancel !== undefined) { + Provision_Cancel.encode(message.cancel, writer.uint32(26).fork()).ldelim() + } + return writer + }, +} + +export const Provision_Complete = { + encode( + message: Provision_Complete, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.state.length !== 0) { + writer.uint32(10).bytes(message.state) + } + if (message.error !== "") { + writer.uint32(18).string(message.error) + } + for (const v of message.resources) { + Resource.encode(v!, writer.uint32(26).fork()).ldelim() + } + for (const v of message.parameters) { + RichParameter.encode(v!, writer.uint32(34).fork()).ldelim() + } + for (const v of message.gitAuthProviders) { + writer.uint32(42).string(v!) + } + if (message.plan.length !== 0) { + writer.uint32(50).bytes(message.plan) + } + return writer + }, +} + +export const Provision_Response = { + encode( + message: Provision_Response, + writer: _m0.Writer = _m0.Writer.create(), + ): _m0.Writer { + if (message.log !== undefined) { + Log.encode(message.log, writer.uint32(10).fork()).ldelim() + } + if (message.complete !== undefined) { + Provision_Complete.encode( + message.complete, + writer.uint32(18).fork(), + ).ldelim() + } + return writer + }, +} + +export interface Provisioner { + Parse(request: Parse_Request): Observable + Provision( + request: Observable, + ): Observable +} diff --git a/site/e2e/tests/app.spec.ts b/site/e2e/tests/app.spec.ts new file mode 100644 index 0000000000000..b3646fbac1caa --- /dev/null +++ b/site/e2e/tests/app.spec.ts @@ -0,0 +1,52 @@ +import { test } from "@playwright/test" +import { randomUUID } from "crypto" +import * as http from "http" +import { createTemplate, createWorkspace, startAgent } from "../helpers" + +test("app", async ({ context, page }) => { + const appContent = "Hello World" + const token = randomUUID() + const srv = http + .createServer((req, res) => { + res.writeHead(200, { "Content-Type": "text/plain" }) + res.end(appContent) + }) + .listen(0) + const addr = srv.address() + if (typeof addr !== "object" || !addr) { + throw new Error("Expected addr to be an object") + } + const appName = "test-app" + const template = await createTemplate(page, { + apply: [ + { + complete: { + resources: [ + { + agents: [ + { + token, + apps: [ + { + url: "http://localhost:" + addr.port, + displayName: appName, + }, + ], + }, + ], + }, + ], + }, + }, + ], + }) + await createWorkspace(page, template) + await startAgent(page, token) + + // Wait for the web terminal to open in a new tab + const pagePromise = context.waitForEvent("page") + await page.getByText(appName).click() + const app = await pagePromise + await app.waitForLoadState("networkidle") + await app.getByText(appContent).isVisible() +}) diff --git a/site/e2e/tests/createWorkspace.spec.ts b/site/e2e/tests/createWorkspace.spec.ts new file mode 100644 index 0000000000000..3317691300f5f --- /dev/null +++ b/site/e2e/tests/createWorkspace.spec.ts @@ -0,0 +1,19 @@ +import { test } from "@playwright/test" +import { createTemplate, createWorkspace } from "../helpers" + +test("create workspace", async ({ page }) => { + const template = await createTemplate(page, { + apply: [ + { + complete: { + resources: [ + { + name: "example", + }, + ], + }, + }, + ], + }) + await createWorkspace(page, template) +}) diff --git a/site/e2e/tests/listTemplates.spec.ts b/site/e2e/tests/listTemplates.spec.ts index 4d3569bfb47f3..e00b1e5d24cf5 100644 --- a/site/e2e/tests/listTemplates.spec.ts +++ b/site/e2e/tests/listTemplates.spec.ts @@ -1,7 +1,4 @@ import { test, expect } from "@playwright/test" -import { getStatePath } from "../helpers" - -test.use({ storageState: getStatePath("authState") }) test("list templates", async ({ page, baseURL }) => { await page.goto(`${baseURL}/templates`, { waitUntil: "networkidle" }) diff --git a/site/e2e/tests/logout.spec.ts b/site/e2e/tests/logout.spec.ts deleted file mode 100644 index 5f8f4c892afeb..0000000000000 --- a/site/e2e/tests/logout.spec.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test, expect } from "@playwright/test" -import { getStatePath } from "../helpers" - -test.use({ storageState: getStatePath("authState") }) - -test("signing out redirects to login page", async ({ page, baseURL }) => { - await page.goto(`${baseURL}/`, { waitUntil: "networkidle" }) - - await page.getByTestId("user-dropdown-trigger").click() - await page.getByRole("menuitem", { name: "Sign Out" }).click() - - await expect( - page.getByRole("heading", { name: "Sign in to Coder" }), - ).toBeVisible() - - expect(page.url()).toMatch(/\/login$/) // ensure we're on the login page with no query params -}) diff --git a/site/e2e/tests/webTerminal.spec.ts b/site/e2e/tests/webTerminal.spec.ts new file mode 100644 index 0000000000000..81a450fcec5c2 --- /dev/null +++ b/site/e2e/tests/webTerminal.spec.ts @@ -0,0 +1,40 @@ +import { expect, test } from "@playwright/test" +import { createTemplate, createWorkspace, startAgent } from "../helpers" +import { randomUUID } from "crypto" + +test("web terminal", async ({ context, page }) => { + const token = randomUUID() + const template = await createTemplate(page, { + apply: [ + { + complete: { + resources: [ + { + agents: [ + { + token, + }, + ], + }, + ], + }, + }, + ], + }) + await createWorkspace(page, template) + await startAgent(page, token) + + // Wait for the web terminal to open in a new tab + const pagePromise = context.waitForEvent("page") + await page.getByTestId("terminal").click() + const terminal = await pagePromise + await terminal.waitForLoadState("networkidle") + + // Ensure that we can type in it + await terminal.keyboard.type("echo hello") + await terminal.keyboard.press("Enter") + + // Make sure the text came back + const number = await terminal.locator("text=hello").all() + expect(number.length).toBe(2) +}) diff --git a/site/package.json b/site/package.json index 8f7f8f4f7c44a..13bb643ffcd87 100644 --- a/site/package.json +++ b/site/package.json @@ -19,6 +19,7 @@ "lint:types": "tsc --noEmit", "playwright:install": "playwright install --with-deps chromium", "playwright:test": "playwright test --config=e2e/playwright.config.ts", + "gen:provisioner": "protoc --plugin=./node_modules/.bin/protoc-gen-ts-proto --ts_proto_out=./e2e/ --ts_proto_opt=outputJsonMethods=false,outputEncodeMethods=encode-no-creation,outputClientImpl=false,nestJs=false,outputPartialMethods=false,fileSuffix=Generated,suffix=hey -I ../provisionersdk/proto ../provisionersdk/proto/provisioner.proto && prettier --cache --write './e2e/provisionerGenerated.ts'", "storybook": "STORYBOOK=true storybook dev -p 6006", "storybook:build": "storybook build", "test": "jest --selectProjects test", @@ -32,9 +33,9 @@ "dependencies": { "@emoji-mart/data": "1.0.5", "@emoji-mart/react": "1.0.1", - "@fastly/performance-observer-polyfill": "2.0.0", "@emotion/react": "11.10.8", "@emotion/styled": "11.11.0", + "@fastly/performance-observer-polyfill": "2.0.0", "@fontsource/ibm-plex-mono": "4.5.10", "@fontsource/inter": "5.0.2", "@monaco-editor/react": "4.5.0", @@ -69,7 +70,6 @@ "jest-location-mock": "1.0.9", "just-debounce-it": "3.1.1", "lodash": "4.17.21", - "playwright": "1.29.2", "react": "18.2.0", "react-chartjs-2": "4.3.1", "react-color": "2.19.3", @@ -99,7 +99,7 @@ "yup": "0.32.11" }, "devDependencies": { - "@playwright/test": "1.29.2", + "@playwright/test": "1.35.1", "@storybook/addon-actions": "7.0.4", "@storybook/addon-essentials": "7.0.4", "@storybook/addon-links": "7.0.4", @@ -150,6 +150,7 @@ "semver": "7.3.7", "storybook": "7.0.4", "storybook-react-context": "0.6.0", + "ts-proto": "1.150.0", "typescript": "4.8.2", "vite-plugin-checker": "0.6.0" }, diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts index 77d720c56feca..aabe1eab29117 100644 --- a/site/src/api/typesGenerated.ts +++ b/site/src/api/typesGenerated.ts @@ -613,6 +613,7 @@ export interface PrometheusConfig { // From codersdk/deployment.go export interface ProvisionerConfig { readonly daemons: number + readonly daemons_echo: boolean readonly daemon_poll_interval: number readonly daemon_poll_jitter: number readonly force_cancel_interval: number diff --git a/site/src/components/FileUpload/FileUpload.tsx b/site/src/components/FileUpload/FileUpload.tsx index 16b5263fa9376..986018df7628d 100644 --- a/site/src/components/FileUpload/FileUpload.tsx +++ b/site/src/components/FileUpload/FileUpload.tsx @@ -117,6 +117,7 @@ export const FileUpload: FC = ({ = ({ color="primary" type="submit" disabled={submitDisabled} + data-testid="form-submit" > {submitLabel} diff --git a/site/src/components/Resources/AgentStatus.tsx b/site/src/components/Resources/AgentStatus.tsx index dcd2317112b39..7a8b80a3a57f1 100644 --- a/site/src/components/Resources/AgentStatus.tsx +++ b/site/src/components/Resources/AgentStatus.tsx @@ -26,6 +26,7 @@ const ReadyLifecycle = () => { return (
diff --git a/site/src/components/TerminalLink/TerminalLink.tsx b/site/src/components/TerminalLink/TerminalLink.tsx index 4e11d09a08b10..f5227302c64b8 100644 --- a/site/src/components/TerminalLink/TerminalLink.tsx +++ b/site/src/components/TerminalLink/TerminalLink.tsx @@ -44,6 +44,7 @@ export const TerminalLink: FC> = ({ "width=900,height=600", ) }} + data-testid="terminal" > {Language.linkText} diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 6b4d6eda9d090..ec50d9d7404b0 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -51,6 +51,7 @@ export const WorkspaceStatusText: FC< = ({ defaultChecked value={form.values.trial} onChange={form.handleChange} + data-testid="trial" />
@@ -110,7 +111,12 @@ export const SetupPageView: React.FC = ({ - + {Language.create} diff --git a/site/src/utils/tar.test.ts b/site/src/utils/tar.test.ts index d431748390d5b..e5bdeebb0dc68 100644 --- a/site/src/utils/tar.test.ts +++ b/site/src/utils/tar.test.ts @@ -15,7 +15,7 @@ test("tar", async () => { group: "codergroup", mode: parseInt("777", 8), }) - const blob = await writer.write() + const blob = (await writer.write()) as Blob // Read const reader = new TarReader() diff --git a/site/src/utils/tar.ts b/site/src/utils/tar.ts index 010428cd18622..57b59adac37b3 100644 --- a/site/src/utils/tar.ts +++ b/site/src/utils/tar.ts @@ -232,7 +232,12 @@ export class TarWriter { view.set(data, offset + 512) offset += 512 + 512 * Math.floor((item.size + 511) / 512) } - return new Blob([this.buffer], { type: "application/x-tar" }) + // Required so it works in the browser and node. + if (typeof Blob !== "undefined") { + return new Blob([this.buffer], { type: "application/x-tar" }) + } else { + return this.buffer + } } private writeString(str: string, offset: number, size: number) { diff --git a/site/src/xServices/createTemplate/createTemplateXService.ts b/site/src/xServices/createTemplate/createTemplateXService.ts index db537be8e08f5..3d4205fc23f05 100644 --- a/site/src/xServices/createTemplate/createTemplateXService.ts +++ b/site/src/xServices/createTemplate/createTemplateXService.ts @@ -11,6 +11,7 @@ import { import { ProvisionerJob, ProvisionerJobLog, + ProvisionerType, Template, TemplateExample, TemplateVersion, @@ -33,6 +34,10 @@ import { assign, createMachine } from "xstate" // 5.create template with the successful template version ID // https://github.com/coder/coder/blob/b6703b11c6578b2f91a310d28b6a7e57f0069be6/cli/templatecreate.go#L169-L170 +const provisioner: ProvisionerType = + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Playwright needs to use a different provisioner type! + typeof (window as any).playwright !== "undefined" ? "echo" : "terraform" + export interface CreateTemplateData { name: string display_name: string @@ -356,7 +361,7 @@ export const createTemplateMachine = return createTemplateVersion(organizationId, { storage_method: "file", example_id: exampleId, - provisioner: "terraform", + provisioner: provisioner, tags: {}, }) } @@ -371,7 +376,7 @@ export const createTemplateMachine = return createTemplateVersion(organizationId, { storage_method: "file", file_id: version.job.file_id, - provisioner: "terraform", + provisioner: provisioner, tags: {}, }) } @@ -380,7 +385,7 @@ export const createTemplateMachine = return createTemplateVersion(organizationId, { storage_method: "file", file_id: uploadResponse.hash, - provisioner: "terraform", + provisioner: provisioner, tags: {}, }) } @@ -402,7 +407,7 @@ export const createTemplateMachine = return createTemplateVersion(organizationId, { storage_method: "file", file_id: version.job.file_id, - provisioner: "terraform", + provisioner: provisioner, user_variable_values: templateData.user_variable_values, tags: {}, }) diff --git a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts index c3ff74fa76197..853b417423f90 100644 --- a/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts +++ b/site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts @@ -326,7 +326,7 @@ export const templateVersionEditorMachine = createMachine( tar.addFolder(fullPath) }) - const blob = await tar.write() + const blob = (await tar.write()) as Blob return API.uploadTemplateFile(new File([blob], "template.tar")) }, createBuild: (ctx) => { diff --git a/site/yarn.lock b/site/yarn.lock index 740ebb2dd4cfb..9db5108cb7f19 100644 --- a/site/yarn.lock +++ b/site/yarn.lock @@ -1919,19 +1919,74 @@ tiny-glob "^0.2.9" tslib "^2.4.0" -"@playwright/test@1.29.2": - version "1.29.2" - resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.29.2.tgz#c48184721d0f0b7627a886e2ec42f1efb2be339d" - integrity sha512-+3/GPwOgcoF0xLz/opTnahel1/y42PdcgZ4hs+BZGIUjtmEFSXGg+nFoaH3NSmuc7a6GSFwXDJ5L7VXpqzigNg== +"@playwright/test@1.35.1": + version "1.35.1" + resolved "https://registry.yarnpkg.com/@playwright/test/-/test-1.35.1.tgz#a596b61e15b980716696f149cc7a2002f003580c" + integrity sha512-b5YoFe6J9exsMYg0pQAobNDR85T1nLumUYgUTtKm4d21iX2L7WqKq9dW8NGJ+2vX0etZd+Y7UeuqsxDXm9+5ZA== dependencies: "@types/node" "*" - playwright-core "1.29.2" + playwright-core "1.35.1" + optionalDependencies: + fsevents "2.3.2" "@popperjs/core@^2.11.7": version "2.11.7" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.7.tgz#ccab5c8f7dc557a52ca3288c10075c9ccd37fff7" integrity sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw== +"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf" + integrity sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ== + +"@protobufjs/base64@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/base64/-/base64-1.1.2.tgz#4c85730e59b9a1f1f349047dbf24296034bb2735" + integrity sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg== + +"@protobufjs/codegen@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@protobufjs/codegen/-/codegen-2.0.4.tgz#7ef37f0d010fb028ad1ad59722e506d9262815cb" + integrity sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg== + +"@protobufjs/eventemitter@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz#355cbc98bafad5978f9ed095f397621f1d066b70" + integrity sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q== + +"@protobufjs/fetch@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/fetch/-/fetch-1.1.0.tgz#ba99fb598614af65700c1619ff06d454b0d84c45" + integrity sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ== + dependencies: + "@protobufjs/aspromise" "^1.1.1" + "@protobufjs/inquire" "^1.1.0" + +"@protobufjs/float@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@protobufjs/float/-/float-1.0.2.tgz#5e9e1abdcb73fc0a7cb8b291df78c8cbd97b87d1" + integrity sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ== + +"@protobufjs/inquire@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/inquire/-/inquire-1.1.0.tgz#ff200e3e7cf2429e2dcafc1140828e8cc638f089" + integrity sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q== + +"@protobufjs/path@^1.1.2": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@protobufjs/path/-/path-1.1.2.tgz#6cc2b20c5c9ad6ad0dccfd21ca7673d8d7fbf68d" + integrity sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA== + +"@protobufjs/pool@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/pool/-/pool-1.1.0.tgz#09fd15f2d6d3abfa9b65bc366506d6ad7846ff54" + integrity sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw== + +"@protobufjs/utf8@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" + integrity sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw== + "@remix-run/router@1.6.3": version "1.6.3" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.6.3.tgz#8205baf6e17ef93be35bf62c37d2d594e9be0dad" @@ -3218,6 +3273,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.191.tgz#09511e7f7cba275acd8b419ddac8da9a6a79e2fa" integrity sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ== +"@types/long@^4.0.1": + version "4.0.2" + resolved "https://registry.yarnpkg.com/@types/long/-/long-4.0.2.tgz#b74129719fc8d11c01868010082d483b7545591a" + integrity sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA== + "@types/mdast@^3.0.0": version "3.0.10" resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" @@ -3268,6 +3328,11 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.18.22.tgz#fd2a15dca290fc9ad565b672fde746191cd0c6e6" integrity sha512-qzaYbXVzin6EPjghf/hTdIbnVW1ErMx8rPzwRNJhlbyJhu2SyqlvjGOY/tbUt6VFyzg56lROcOeSQRInpt63Yw== +"@types/node@>=13.7.0": + version "20.3.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.3.1.tgz#e8a83f1aa8b649377bb1fb5d7bac5cb90e784dfe" + integrity sha512-EhcH/wvidPy1WeML3TtYFGR83UzjxeWRen9V402T8aUGYsCHOmfoisV3ZSg03gAFIbLq8TnWOJ0f4cALtnSEUg== + "@types/node@^13.7.0": version "13.13.52" resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.52.tgz#03c13be70b9031baaed79481c0c0cfb0045e53f7" @@ -3288,6 +3353,11 @@ resolved "https://registry.yarnpkg.com/@types/npmlog/-/npmlog-4.1.4.tgz#30eb872153c7ead3e8688c476054ddca004115f6" integrity sha512-WKG4gTr8przEZBiJ5r3s8ZIAoMXNbOgQ+j/d5O4X3x6kZJRLNvyUJuUK/KoG3+8BaOHPhp2m7WC6JKKeovDSzQ== +"@types/object-hash@^3.0.2": + version "3.0.2" + resolved "https://registry.yarnpkg.com/@types/object-hash/-/object-hash-3.0.2.tgz#f3656433e6c6049571fc3fb3fb42f389af96c0eb" + integrity sha512-tfyXl1JPCf2hzIDK29gO7qGqJjThKBzg/Cn3bA68R9NmWdOx+f7k5mm4to/n43BHspCwcoUC6FU4NpUoK/h9bQ== + "@types/parse-json@^4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" @@ -4513,6 +4583,11 @@ canvas@2.11.0: nan "^2.17.0" simple-get "^3.0.3" +case-anything@^2.1.10: + version "2.1.13" + resolved "https://registry.yarnpkg.com/case-anything/-/case-anything-2.1.13.tgz#0cdc16278cb29a7fcdeb072400da3f342ba329e9" + integrity sha512-zlOQ80VrQ2Ue+ymH5OuM/DlDq64mEm+B9UTdHULv5osUMD6HalNTblf2b1u/m6QecjsnOkBpqVZ+XPwIVsy7Ng== + ccount@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" @@ -5041,6 +5116,11 @@ data-urls@^3.0.2: whatwg-mimetype "^3.0.0" whatwg-url "^11.0.0" +dataloader@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-1.4.0.tgz#bca11d867f5d3f1b9ed9f737bd15970c65dff5c8" + integrity sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw== + date-fns@2.30.0: version "2.30.0" resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0" @@ -5225,6 +5305,11 @@ detect-indent@^6.1.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== +detect-libc@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" + integrity sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg== + detect-libc@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" @@ -5338,6 +5423,13 @@ dotenv@^16.0.0: resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== +dprint-node@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/dprint-node/-/dprint-node-1.0.7.tgz#f571eaf61affb3a696cff1bdde78a021875ba540" + integrity sha512-NTZOW9A7ipb0n7z7nC3wftvsbceircwVHSgzobJsEQa+7RnOMbhrfX5IflA6CtC4GA63DSAiHYXa4JKEy9F7cA== + dependencies: + detect-libc "^1.0.3" + duplexify@^3.5.0, duplexify@^3.6.0: version "3.7.1" resolved "https://registry.yarnpkg.com/duplexify/-/duplexify-3.7.1.tgz#2a4df5317f6ccfd91f86d6fd25d8d8a103b88309" @@ -6320,7 +6412,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@^2.3.2, fsevents@~2.3.2: +fsevents@2.3.2, fsevents@^2.3.2, fsevents@~2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== @@ -8261,6 +8353,11 @@ log-symbols@^4.1.0: chalk "^4.1.0" is-unicode-supported "^0.1.0" +long@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/long/-/long-4.0.0.tgz#9a7b71cfb7d361a194ea555241c92f7468d5bf28" + integrity sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA== + longest-streak@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" @@ -9545,17 +9642,10 @@ pkg-dir@^5.0.0: dependencies: find-up "^5.0.0" -playwright-core@1.29.2: - version "1.29.2" - resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.29.2.tgz#2e8347e7e8522409f22b244e600e703b64022406" - integrity sha512-94QXm4PMgFoHAhlCuoWyaBYKb92yOcGVHdQLoxQ7Wjlc7Flg4aC/jbFW7xMR52OfXMVkWicue4WXE7QEegbIRA== - -playwright@1.29.2: - version "1.29.2" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.29.2.tgz#d6a0a3e8e44f023f7956ed19ffa8af915a042769" - integrity sha512-hKBYJUtdmYzcjdhYDkP9WGtORwwZBBKAW8+Lz7sr0ZMxtJr04ASXVzH5eBWtDkdb0c3LLFsehfPBTRfvlfKJOA== - dependencies: - playwright-core "1.29.2" +playwright-core@1.35.1: + version "1.35.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.35.1.tgz#52c1e6ffaa6a8c29de1a5bdf8cce0ce290ffb81d" + integrity sha512-pNXb6CQ7OqmGDRspEjlxE49w+4YtR6a3X6mT1hZXeJHWmsEz7SunmvZeiG/+y1yyMZdHnnn73WKYdtV1er0Xyg== pluralize@^7.0.0: version "7.0.0" @@ -9727,6 +9817,25 @@ property-information@^6.0.0: resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d" integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg== +protobufjs@^6.11.3, protobufjs@^6.8.8: + version "6.11.3" + resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-6.11.3.tgz#637a527205a35caa4f3e2a9a4a13ddffe0e7af74" + integrity sha512-xL96WDdCZYdU7Slin569tFX712BxsxslWwAfAhCYjQKGTq7dAU91Lomy6nLLhh/dyGhk/YH4TwTSRxTzhuHyZg== + dependencies: + "@protobufjs/aspromise" "^1.1.2" + "@protobufjs/base64" "^1.1.2" + "@protobufjs/codegen" "^2.0.4" + "@protobufjs/eventemitter" "^1.1.0" + "@protobufjs/fetch" "^1.1.0" + "@protobufjs/float" "^1.0.2" + "@protobufjs/inquire" "^1.1.0" + "@protobufjs/path" "^1.1.2" + "@protobufjs/pool" "^1.1.0" + "@protobufjs/utf8" "^1.1.0" + "@types/long" "^4.0.1" + "@types/node" ">=13.7.0" + long "^4.0.0" + proxy-addr@~2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025" @@ -11330,6 +11439,34 @@ ts-morph@^13.0.1: "@ts-morph/common" "~0.12.3" code-block-writer "^11.0.0" +ts-poet@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/ts-poet/-/ts-poet-6.4.1.tgz#e68d314a07cf9c0d568a3bfd87023ec91ff77964" + integrity sha512-AjZEs4h2w4sDfwpHMxQKHrTlNh2wRbM5NRXmLz0RiH+yPGtSQFbe9hBpNocU8vqVNgfh0BIOiXR80xDz3kKxUQ== + dependencies: + dprint-node "^1.0.7" + +ts-proto-descriptors@1.9.0: + version "1.9.0" + resolved "https://registry.yarnpkg.com/ts-proto-descriptors/-/ts-proto-descriptors-1.9.0.tgz#0ed5631f11851846c8de21be2bff346719edce71" + integrity sha512-Ui8zA5Q4Jnq6JIGRraUWvECrqixxtwwin8GkhIkvwCpR+JcSPsxWe8HfTj5eHfyruGYI6Zjf96XlC87hTakHfQ== + dependencies: + long "^4.0.0" + protobufjs "^6.8.8" + +ts-proto@1.150.0: + version "1.150.0" + resolved "https://registry.yarnpkg.com/ts-proto/-/ts-proto-1.150.0.tgz#41b9a737caa5bc242274eda01749e4ecfc1a4fa2" + integrity sha512-EYnKWkNkWRmnK2nG5D9J1zN959YD/gff+e8/nqpOw7kdrfsnCe1dr/+EYxiRhPnq/umVpBLwI/KaSDg9G+9KPA== + dependencies: + "@types/object-hash" "^3.0.2" + case-anything "^2.1.10" + dataloader "^1.4.0" + object-hash "^3.0.0" + protobufjs "^6.11.3" + ts-poet "^6.4.1" + ts-proto-descriptors "1.9.0" + ts-prune@0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/ts-prune/-/ts-prune-0.10.3.tgz#b6c71a525543b38dcf947a7d3adfb7f9e8b91f38" From db0ab735570f7fef0ad43fa6fa226fa4a855eb7c Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Wed, 21 Jun 2023 23:45:22 +0000 Subject: [PATCH 2/3] Fix web terminal flake --- .prettierignore | 3 +++ .prettierignore.include | 3 +++ site/.eslintignore | 3 +++ site/.prettierignore | 3 +++ site/e2e/helpers.ts | 2 +- site/e2e/tests/webTerminal.spec.ts | 15 +++++++++++---- 6 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.prettierignore b/.prettierignore index fe39bea750359..176b414124a29 100644 --- a/.prettierignore +++ b/.prettierignore @@ -75,3 +75,6 @@ helm/templates/*.yaml # Testdata shouldn't be formatted. scripts/apitypings/testdata/**/*.ts + +# Generated files shouldn't be formatted. +site/e2e/provisionerGenerated.ts diff --git a/.prettierignore.include b/.prettierignore.include index 74e477479c311..a299e3a2b70c8 100644 --- a/.prettierignore.include +++ b/.prettierignore.include @@ -8,3 +8,6 @@ helm/templates/*.yaml # Testdata shouldn't be formatted. scripts/apitypings/testdata/**/*.ts + +# Generated files shouldn't be formatted. +site/e2e/provisionerGenerated.ts diff --git a/site/.eslintignore b/site/.eslintignore index 359b7c3e0eea2..eff2edeade4a2 100644 --- a/site/.eslintignore +++ b/site/.eslintignore @@ -75,3 +75,6 @@ stats/ # Testdata shouldn't be formatted. ../scripts/apitypings/testdata/**/*.ts + +# Generated files shouldn't be formatted. +e2e/provisionerGenerated.ts diff --git a/site/.prettierignore b/site/.prettierignore index 359b7c3e0eea2..eff2edeade4a2 100644 --- a/site/.prettierignore +++ b/site/.prettierignore @@ -75,3 +75,6 @@ stats/ # Testdata shouldn't be formatted. ../scripts/apitypings/testdata/**/*.ts + +# Generated files shouldn't be formatted. +e2e/provisionerGenerated.ts diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 00adb555c9c36..2c16aa606fba7 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -73,7 +73,7 @@ export const startAgent = async (page: Page, token: string): Promise => { const cp = spawn("go", ["run", coderMain, "agent", "--no-reap"], { env: { ...process.env, - CODER_AGENT_URL: "http://localhost:"+port, + CODER_AGENT_URL: "http://localhost:" + port, CODER_AGENT_TOKEN: token, }, }) diff --git a/site/e2e/tests/webTerminal.spec.ts b/site/e2e/tests/webTerminal.spec.ts index 81a450fcec5c2..492634b293a3e 100644 --- a/site/e2e/tests/webTerminal.spec.ts +++ b/site/e2e/tests/webTerminal.spec.ts @@ -1,4 +1,4 @@ -import { expect, test } from "@playwright/test" +import { test } from "@playwright/test" import { createTemplate, createWorkspace, startAgent } from "../helpers" import { randomUUID } from "crypto" @@ -34,7 +34,14 @@ test("web terminal", async ({ context, page }) => { await terminal.keyboard.type("echo hello") await terminal.keyboard.press("Enter") - // Make sure the text came back - const number = await terminal.locator("text=hello").all() - expect(number.length).toBe(2) + const locator = terminal.locator("text=hello") + + for (let i = 0; i < 10; i++) { + const items = await locator.all() + // Make sure the text came back + if (items.length === 2) { + break + } + await new Promise((r) => setTimeout(r, 250)) + } }) From 2ae14bc795e6189e5bf723253839e0903a56d312 Mon Sep 17 00:00:00 2001 From: Kyle Carberry Date: Thu, 22 Jun 2023 00:14:33 +0000 Subject: [PATCH 3/3] Fix tunnel URL --- site/e2e/playwright.config.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index a0cfcb7f197f5..a8d2472b502cc 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -31,8 +31,11 @@ const config = defineConfig({ }, webServer: { command: - `go run -tags embed ${coderMain} server --global-config ` + - `$(mktemp -d -t e2e-XXXXXXXXXX) --in-memory --telemetry=false ` + + `go run -tags embed ${coderMain} server ` + + `--global-config $(mktemp -d -t e2e-XXXXXXXXXX) ` + + `--access-url=http://localhost:${port} ` + + `--http-address=localhost:${port} ` + + `--in-memory --telemetry=false ` + `--provisioner-daemons 10 ` + `--provisioner-daemons-echo ` + `--provisioner-daemon-poll-interval 50ms`,