Skip to content

feat: Implement basic e2e scenario #7199

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 21 commits into from
Closed
26 changes: 25 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -608,13 +608,37 @@ jobs:
working-directory: site

- name: Upload Playwright Failed Tests
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux' && !github.event.pull_request.head.repo.fork
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
uses: actions/upload-artifact@v3
with:
name: failed-test-videos
path: ./site/test-results/**/*.webm
retention-days: 7

- run: docker ps -a
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
working-directory: site

- run: docker exec coder-admin-my-first-workspace ps aux
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
working-directory: site

- run: docker exec coder-admin-my-first-workspace ls -la /tmp/
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
working-directory: site

- run: docker exec coder-admin-my-first-workspace cat '/tmp/coder-agent-init.log'
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
working-directory: site

- run: docker exec coder-admin-my-first-workspace cat '/tmp/coder-agent.log'
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
working-directory: site

- run: docker exec coder-admin-my-first-workspace cat '/tmp/coder-startup-script.log'
if: always() && github.actor != 'dependabot[bot]' && runner.os == 'Linux'
working-directory: site

chromatic:
# REMARK: this is only used to build storybook and deploy it to Chromatic.
runs-on: ubuntu-latest
Expand Down
2 changes: 1 addition & 1 deletion provisionersdk/scripts/bootstrap_linux.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ while :; do
# will have available.
status=""
if command -v curl >/dev/null 2>&1; then
curl -fsSL --compressed "${BINARY_URL}" -o "${BINARY_NAME}" && break
curl -fsSL --compressed "${BINARY_URL}" -o "${BINARY_NAME}" -v 2>>/tmp/coder-startup-script.log && break
status=$?
elif command -v wget >/dev/null 2>&1; then
wget -q "${BINARY_URL}" -O "${BINARY_NAME}" && break
Expand Down
1 change: 1 addition & 0 deletions site/e2e/constants.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Default port from the server
export const defaultPort = 3000
export const defaultEndpoint = `localhost:${defaultPort}`

// Credentials for the first user
export const username = "admin"
Expand Down
24 changes: 0 additions & 24 deletions site/e2e/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,5 @@
import { Page } from "@playwright/test"
import path from "path"

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",
}

export const clickButton = async (page: Page, name: string): Promise<void> => {
await page.getByRole("button", { name, exact: true }).click()
}

export const fillInput = async (
page: Page,
label: string,
value: string,
): Promise<void> => {
await page.fill(`text=${label}`, value)
}

const statesDir = path.join(__dirname, "./states")

export const getStatePath = (name: string): string => {
Expand Down
11 changes: 8 additions & 3 deletions site/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { PlaywrightTestConfig } from "@playwright/test"
import path from "path"
import { defaultPort } from "./constants"
import { defaultPort, defaultEndpoint } from "./constants"

const port = process.env.CODER_E2E_PORT
? Number(process.env.CODER_E2E_PORT)
Expand All @@ -12,14 +12,19 @@ const config: PlaywrightTestConfig = {
testDir: "tests",
globalSetup: require.resolve("./globalSetup"),
use: {
baseURL: `http://localhost:${port}`,
video: "retain-on-failure",
baseURL: `http://${defaultEndpoint}`,
video: {
mode: "retain-on-failure",
size: { width: 1280, height: 768 },
},
viewport: { width: 1280, height: 768 },
},
webServer: {
command: `go run -tags embed ${coderMain} server --global-config $(mktemp -d -t e2e-XXXXXXXXXX)`,
port,
reuseExistingServer: false,
},
timeout: 10 * 60 * 1000,
}

export default config
36 changes: 36 additions & 0 deletions site/e2e/pom/CreateTemplatePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { expect, Locator, Page } from "@playwright/test"
import { BasePom } from "./BasePom"

export class CreateTemplatePage extends BasePom {
readonly createTemplateForm: Locator
readonly submitButton: Locator

constructor(baseURL: string | undefined, page: Page) {
super(baseURL, `/templates`, page)

this.createTemplateForm = page.getByTestId("form-create-template")
this.submitButton = page.getByTestId("button-create-template")
}

async loaded() {
await expect(this.page).toHaveTitle("Create Template - Coder")

await this.createTemplateForm.waitFor({ state: "visible" })
await this.submitButton.waitFor({ state: "visible" })
}

async submitForm() {
await this.createTemplateForm
.getByTestId("file-upload")
.setInputFiles("./e2e/testdata/docker.tar")
await this.createTemplateForm.getByLabel("Name *").fill("my-first-template")
await this.createTemplateForm
.getByLabel("Display name")
.fill("My First Template")
await this.createTemplateForm
.getByLabel("Description")
.fill("This is my first template.")

await this.submitButton.click()
}
}
29 changes: 29 additions & 0 deletions site/e2e/pom/CreateWorkspacePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { expect, Locator, Page } from "@playwright/test"
import { BasePom } from "./BasePom"

export class CreateWorkspacePage extends BasePom {
readonly createWorkspaceForm: Locator
readonly submitButton: Locator

constructor(baseURL: string | undefined, page: Page) {
super(baseURL, `/templates/docker/workspace`, page)

this.createWorkspaceForm = page.getByTestId("form-create-workspace")
this.submitButton = page.getByTestId("button-create-workspace")
}

async loaded() {
await expect(this.page).toHaveTitle("Create Workspace - Coder")

await this.createWorkspaceForm.waitFor({ state: "visible" })
await this.submitButton.waitFor({ state: "visible" })
}

async submitForm() {
await this.createWorkspaceForm
.getByLabel("Workspace Name")
.fill("my-first-workspace")

await this.submitButton.click()
}
}
17 changes: 0 additions & 17 deletions site/e2e/pom/SignInPage.ts

This file was deleted.

22 changes: 22 additions & 0 deletions site/e2e/pom/TemplatePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { expect, Locator, Page } from "@playwright/test"
import { BasePom } from "./BasePom"

export class TemplatePage extends BasePom {
readonly createWorkspaceButton: Locator

constructor(baseURL: string | undefined, page: Page) {
super(baseURL, `/templates/docker`, page)

this.createWorkspaceButton = page.getByTestId("button-create-workspace")
}

async loaded() {
await this.createWorkspaceButton.waitFor({ state: "visible" })

await expect(this.page).toHaveTitle("My First Template · Template - Coder")
}

async createWorkspace() {
await this.createWorkspaceButton.click()
}
}
26 changes: 26 additions & 0 deletions site/e2e/pom/TemplatesPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { expect, Locator, Page } from "@playwright/test"
import { BasePom } from "./BasePom"

export class TemplatesPage extends BasePom {
readonly addTemplateButton: Locator

constructor(baseURL: string | undefined, page: Page) {
super(baseURL, `/templates`, page)

this.addTemplateButton = page.getByTestId("button-add-template")
}

async goto() {
await this.page.goto(this.url, { waitUntil: "networkidle" })
}

async loaded() {
await this.addTemplateButton.waitFor({ state: "visible" })

await expect(this.page).toHaveTitle("Templates - Coder")
}

async addTemplate() {
await this.addTemplateButton.click()
}
}
82 changes: 82 additions & 0 deletions site/e2e/pom/WorkspacePage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { expect, Locator, Page } from "@playwright/test"
import { BasePom } from "./BasePom"

export class WorkspacePage extends BasePom {
readonly workspaceOptionsButton: Locator
readonly deleteWorkspaceMenuItem: Locator
readonly stopWorkspaceButton: Locator

readonly workspaceRunningBadge: Locator
readonly workspaceStoppedBadge: Locator
readonly workspaceDeletedBadge: Locator

readonly terminalButton: Locator
readonly agentVersion: Locator
readonly agentLifecycleReady: Locator

readonly deleteDialogConfirmation: Locator
readonly deleteDialogConfirm: Locator

constructor(baseURL: string | undefined, page: Page) {
super(baseURL, `/templates/docker/workspace`, page)

this.workspaceOptionsButton = page.getByTestId("workspace-options-button")
this.deleteWorkspaceMenuItem = page.getByTestId("menuitem-delete-workspace")
this.stopWorkspaceButton = page.getByTestId("button-stop-workspace")

this.workspaceRunningBadge = page.getByTestId(
"badge-workspace-status-running",
)
this.workspaceStoppedBadge = page.getByTestId(
"badge-workspace-status-stopped",
)
this.workspaceDeletedBadge = page.getByTestId(
"badge-workspace-status-deleted",
)
this.terminalButton = page.getByTestId("button-terminal")
this.agentVersion = page.getByTestId("agent-version")
this.agentLifecycleReady = page.getByTestId("agent-lifecycle-ready")

this.deleteDialogConfirmation = page.getByTestId(
"delete-dialog-confirmation",
)
this.deleteDialogConfirm = page.getByTestId("delete-dialog-confirm")
}

async loaded() {
await this.stopWorkspaceButton.waitFor({ state: "visible" })
await expect(this.page).toHaveTitle("admin/my-first-workspace - Coder")
}

async stop() {
await this.stopWorkspaceButton.click()
}

async delete() {
await this.workspaceOptionsButton.click()
await this.deleteWorkspaceMenuItem.click()

await this.deleteDialogConfirmation.waitFor({ state: "visible" })
await this.deleteDialogConfirmation
.getByLabel("Name of workspace to delete")
.fill("my-first-workspace")

await this.page.waitForTimeout(1000) // Wait for 1s to snapshot the delete dialog before submitting
await this.deleteDialogConfirm.click()
}

async isRunning() {
await this.workspaceRunningBadge.waitFor({ state: "visible" })
await this.terminalButton.waitFor({ state: "visible" })
await this.agentVersion.waitFor({ state: "visible" })
await this.agentLifecycleReady.waitFor({ state: "visible" })
}

async isStopped() {
await this.workspaceStoppedBadge.waitFor({ state: "visible" })
}

async isDeleted() {
await this.workspaceDeletedBadge.waitFor({ state: "visible" })
}
}
2 changes: 0 additions & 2 deletions site/e2e/pom/index.ts

This file was deleted.

Empty file removed site/e2e/states/.gitkeep
Empty file.
Binary file added site/e2e/testdata/docker.tar
Binary file not shown.
53 changes: 53 additions & 0 deletions site/e2e/tests/basicScenario.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { test } from "@playwright/test"
import { getStatePath } from "../helpers"
import { TemplatesPage } from "../pom/TemplatesPage"
import { CreateTemplatePage } from "../pom/CreateTemplatePage"
import { TemplatePage } from "../pom/TemplatePage"
import { CreateWorkspacePage } from "../pom/CreateWorkspacePage"
import { WorkspacePage } from "../pom/WorkspacePage"

test.use({ storageState: getStatePath("authState") })

test("Basic scenario", async ({ page, baseURL }) => {
test.slow()

const templatesPage = new TemplatesPage(baseURL, page)
const createTemplatePage = new CreateTemplatePage(baseURL, page)
const templatePage = new TemplatePage(baseURL, page)
const createWorkspacePage = new CreateWorkspacePage(baseURL, page)
const workspacePage = new WorkspacePage(baseURL, page)

await test.step("Load empty templates page", async () => {
await templatesPage.goto()
await templatesPage.loaded()
})

await test.step("Upload a template", async () => {
await templatesPage.addTemplate()
await createTemplatePage.loaded()

await createTemplatePage.submitForm()
await templatePage.loaded()
})

await test.step("Start a workspace", async () => {
await templatePage.createWorkspace()
await createWorkspacePage.loaded()

await createWorkspacePage.submitForm()
await workspacePage.loaded()
await workspacePage.isRunning()
await page.waitForTimeout(1000) // Wait for 1s to snapshot the agent status on the video
})

await test.step("Stop the workspace", async () => {
await workspacePage.stop()
await workspacePage.isStopped()
})

await test.step("Delete the workspace", async () => {
await workspacePage.delete()
await workspacePage.isDeleted()
await page.waitForTimeout(1000) // Wait to show the deleted workspace
})
})
9 changes: 0 additions & 9 deletions site/e2e/tests/listTemplates.spec.ts

This file was deleted.

Loading