mirror of https://github.com/coder/coder.git
feat: add cohesive e2e tests for the web terminal, apps, and workspaces (#8140)
* feat: add cohesive e2e tests for the web terminal, apps, and workspaces * Fix web terminal flake
This commit is contained in:
parent
2a492b7008
commit
d434181941
|
@ -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/
|
||||
|
|
|
@ -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/
|
||||
|
@ -74,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -821,7 +821,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)
|
||||
|
@ -1177,7 +1177,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)
|
||||
|
@ -1192,53 +1191,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() {
|
||||
|
@ -1261,7 +1221,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
|
||||
|
|
|
@ -288,6 +288,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
|
||||
|
|
|
@ -7945,6 +7945,9 @@ const docTemplate = `{
|
|||
"daemons": {
|
||||
"type": "integer"
|
||||
},
|
||||
"daemons_echo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"force_cancel_interval": {
|
||||
"type": "integer"
|
||||
}
|
||||
|
|
|
@ -7120,6 +7120,9 @@
|
|||
"daemons": {
|
||||
"type": "integer"
|
||||
},
|
||||
"daemons_echo": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"force_cancel_interval": {
|
||||
"type": "integer"
|
||||
}
|
||||
|
|
|
@ -310,6 +310,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"`
|
||||
|
@ -1093,6 +1094,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.",
|
||||
|
|
|
@ -283,6 +283,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,
|
||||
|
|
|
@ -1960,6 +1960,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,
|
||||
|
@ -2289,6 +2290,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,
|
||||
|
@ -3125,6 +3127,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
|
||||
}
|
||||
```
|
||||
|
@ -3136,6 +3139,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
|
||||
|
|
|
@ -2,7 +2,7 @@ terraform {
|
|||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = "~> 0.7.0"
|
||||
version = "~> 0.8.3"
|
||||
}
|
||||
docker = {
|
||||
source = "kreuzwerker/docker"
|
||||
|
|
|
@ -30,6 +30,7 @@ storybook-static/
|
|||
test-results/*
|
||||
e2e/test-results/*
|
||||
e2e/states/*.json
|
||||
e2e/.auth.json
|
||||
playwright-report/*
|
||||
.swc
|
||||
dist/
|
||||
|
@ -74,3 +75,6 @@ stats/
|
|||
|
||||
# Testdata shouldn't be formatted.
|
||||
../scripts/apitypings/testdata/**/*.ts
|
||||
|
||||
# Generated files shouldn't be formatted.
|
||||
e2e/provisionerGenerated.ts
|
||||
|
|
|
@ -30,6 +30,7 @@ storybook-static/
|
|||
test-results/*
|
||||
e2e/test-results/*
|
||||
e2e/states/*.json
|
||||
e2e/.auth.json
|
||||
playwright-report/*
|
||||
.swc
|
||||
dist/
|
||||
|
@ -74,3 +75,6 @@ stats/
|
|||
|
||||
# Testdata shouldn't be formatted.
|
||||
../scripts/apitypings/testdata/**/*.ts
|
||||
|
||||
# Generated files shouldn't be formatted.
|
||||
e2e/provisionerGenerated.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 })
|
||||
})
|
|
@ -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<void> => {
|
||||
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
|
|
@ -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: "Create Workspace",
|
||||
createTemplate: "Create Template",
|
||||
createWorkspace: "Create Workspace",
|
||||
submitCreateWorkspace: "Create Workspace",
|
||||
stopWorkspace: "Stop",
|
||||
startWorkspace: "Start",
|
||||
}
|
||||
|
||||
export const clickButton = async (page: Page, name: string): Promise<void> => {
|
||||
await page.getByRole("button", { name, exact: true }).click()
|
||||
}
|
||||
|
||||
export const fillInput = async (
|
||||
// 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,
|
||||
label: string,
|
||||
value: string,
|
||||
): Promise<void> => {
|
||||
await page.fill(`text=${label}`, value)
|
||||
templateName: string,
|
||||
): Promise<string> => {
|
||||
await page.goto("/templates/" + templateName + "/workspace", {
|
||||
waitUntil: "networkidle",
|
||||
})
|
||||
const name = randomName()
|
||||
await page.getByLabel("name").fill(name)
|
||||
await page.getByTestId("form-submit").click()
|
||||
|
||||
await expect(page).toHaveURL("/@admin/" + name)
|
||||
await page.getByTestId("build-status").isVisible()
|
||||
return name
|
||||
}
|
||||
|
||||
const statesDir = path.join(__dirname, "./states")
|
||||
|
||||
export const getStatePath = (name: string): string => {
|
||||
return path.join(statesDir, `${name}.json`)
|
||||
// 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,
|
||||
responses?: EchoProvisionerResponses,
|
||||
): Promise<string> => {
|
||||
// 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<void> => {
|
||||
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<T> = {
|
||||
[P in keyof T]?: T[P] extends (infer U)[]
|
||||
? RecursivePartial<U>[]
|
||||
: T[P] extends object | undefined
|
||||
? RecursivePartial<T[P]>
|
||||
: T[P]
|
||||
}
|
||||
|
||||
interface EchoProvisionerResponses {
|
||||
// parse is for observing any Terraform variables
|
||||
parse?: RecursivePartial<Parse_Response>[]
|
||||
// plan occurs when the template is imported
|
||||
plan?: RecursivePartial<Provision_Response>[]
|
||||
// apply occurs when the workspace is built
|
||||
apply?: RecursivePartial<Provision_Response>[]
|
||||
}
|
||||
|
||||
// createTemplateVersionTar consumes a series of echo provisioner protobufs and
|
||||
// converts it into an uploadable tar file.
|
||||
const createTemplateVersionTar = async (
|
||||
responses?: EchoProvisionerResponses,
|
||||
): Promise<Buffer> => {
|
||||
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<Provision_Response>,
|
||||
) => {
|
||||
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)
|
||||
}
|
||||
|
||||
const randomName = () => {
|
||||
return randomUUID().slice(0, 8)
|
||||
}
|
||||
|
|
|
@ -1,25 +1,47 @@
|
|||
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) ` +
|
||||
`--access-url=http://localhost:${port} ` +
|
||||
`--http-address=localhost:${port} ` +
|
||||
`--in-memory --telemetry=false ` +
|
||||
`--provisioner-daemons 10 ` +
|
||||
`--provisioner-daemons-echo ` +
|
||||
`--provisioner-daemon-poll-interval 50ms`,
|
||||
port,
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
export default config
|
||||
|
|
|
@ -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<Parse_Response>
|
||||
Provision(
|
||||
request: Observable<Provision_Request>,
|
||||
): Observable<Provision_Response>
|
||||
}
|
|
@ -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()
|
||||
})
|
|
@ -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)
|
||||
})
|
|
@ -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" })
|
||||
|
|
|
@ -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
|
||||
})
|
|
@ -0,0 +1,47 @@
|
|||
import { 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")
|
||||
|
||||
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))
|
||||
}
|
||||
})
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -614,6 +614,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
|
||||
|
|
|
@ -117,6 +117,7 @@ export const FileUpload: FC<FileUploadProps> = ({
|
|||
|
||||
<input
|
||||
type="file"
|
||||
data-testid="file-upload"
|
||||
ref={inputRef}
|
||||
className={styles.input}
|
||||
accept={extension}
|
||||
|
|
|
@ -36,6 +36,7 @@ export const FormFooter: FC<FormFooterProps> = ({
|
|||
color="primary"
|
||||
type="submit"
|
||||
disabled={submitDisabled}
|
||||
data-testid="form-submit"
|
||||
>
|
||||
{submitLabel}
|
||||
</LoadingButton>
|
||||
|
|
|
@ -26,6 +26,7 @@ const ReadyLifecycle = () => {
|
|||
return (
|
||||
<div
|
||||
role="status"
|
||||
data-testid="agent-status-ready"
|
||||
aria-label={t("agentStatus.connected.ready") || "Ready"}
|
||||
className={combineClasses([styles.status, styles.connected])}
|
||||
/>
|
||||
|
|
|
@ -44,6 +44,7 @@ export const TerminalLink: FC<React.PropsWithChildren<TerminalLinkProps>> = ({
|
|||
"width=900,height=600",
|
||||
)
|
||||
}}
|
||||
data-testid="terminal"
|
||||
>
|
||||
<SecondaryAgentButton>{Language.linkText}</SecondaryAgentButton>
|
||||
</Link>
|
||||
|
|
|
@ -51,6 +51,7 @@ export const WorkspaceStatusText: FC<
|
|||
<Cond>
|
||||
<span
|
||||
role="status"
|
||||
data-testid="build-status"
|
||||
className={combineClasses([
|
||||
className,
|
||||
styles.root,
|
||||
|
|
|
@ -96,6 +96,7 @@ export const SetupPageView: React.FC<SetupPageViewProps> = ({
|
|||
defaultChecked
|
||||
value={form.values.trial}
|
||||
onChange={form.handleChange}
|
||||
data-testid="trial"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -110,7 +111,12 @@ export const SetupPageView: React.FC<SetupPageViewProps> = ({
|
|||
</Box>
|
||||
</Box>
|
||||
</div>
|
||||
<LoadingButton fullWidth loading={isLoading} type="submit">
|
||||
<LoadingButton
|
||||
fullWidth
|
||||
loading={isLoading}
|
||||
type="submit"
|
||||
data-testid="create"
|
||||
>
|
||||
{Language.create}
|
||||
</LoadingButton>
|
||||
</Stack>
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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: {},
|
||||
})
|
||||
|
|
|
@ -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) => {
|
||||
|
|
171
site/yarn.lock
171
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"
|
||||
|
|
Loading…
Reference in New Issue