diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6ab0b00017c2a..d658bab4cc731 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -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 diff --git a/provisionersdk/scripts/bootstrap_linux.sh b/provisionersdk/scripts/bootstrap_linux.sh index abd91d163d0e0..6f461b97f926a 100644 --- a/provisionersdk/scripts/bootstrap_linux.sh +++ b/provisionersdk/scripts/bootstrap_linux.sh @@ -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 diff --git a/site/e2e/constants.ts b/site/e2e/constants.ts index 1e56c3d8b3b28..405a5203a667f 100644 --- a/site/e2e/constants.ts +++ b/site/e2e/constants.ts @@ -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" diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 1b8defa88c4e2..e3c7091503cc1 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -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 => { - await page.getByRole("button", { name, exact: true }).click() -} - -export const fillInput = async ( - page: Page, - label: string, - value: string, -): Promise => { - await page.fill(`text=${label}`, value) -} - const statesDir = path.join(__dirname, "./states") export const getStatePath = (name: string): string => { diff --git a/site/e2e/playwright.config.ts b/site/e2e/playwright.config.ts index 43c01871dddc0..0730cff9b11ab 100644 --- a/site/e2e/playwright.config.ts +++ b/site/e2e/playwright.config.ts @@ -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) @@ -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 diff --git a/site/e2e/pom/CreateTemplatePage.ts b/site/e2e/pom/CreateTemplatePage.ts new file mode 100644 index 0000000000000..4f34bcd7c34f1 --- /dev/null +++ b/site/e2e/pom/CreateTemplatePage.ts @@ -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() + } +} diff --git a/site/e2e/pom/CreateWorkspacePage.ts b/site/e2e/pom/CreateWorkspacePage.ts new file mode 100644 index 0000000000000..dbb075e011d6f --- /dev/null +++ b/site/e2e/pom/CreateWorkspacePage.ts @@ -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() + } +} diff --git a/site/e2e/pom/SignInPage.ts b/site/e2e/pom/SignInPage.ts deleted file mode 100644 index 362674588f6c0..0000000000000 --- a/site/e2e/pom/SignInPage.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Page } from "@playwright/test" -import { BasePom } from "./BasePom" - -export class SignInPage extends BasePom { - constructor(baseURL: string | undefined, page: Page) { - super(baseURL, "/login", page) - } - - async submitBuiltInAuthentication( - email: string, - password: string, - ): Promise { - await this.page.fill("text=Email", email) - await this.page.fill("text=Password", password) - await this.page.click('button:has-text("Sign In")') - } -} diff --git a/site/e2e/pom/TemplatePage.ts b/site/e2e/pom/TemplatePage.ts new file mode 100644 index 0000000000000..dfd7628a2bf39 --- /dev/null +++ b/site/e2e/pom/TemplatePage.ts @@ -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() + } +} diff --git a/site/e2e/pom/TemplatesPage.ts b/site/e2e/pom/TemplatesPage.ts new file mode 100644 index 0000000000000..2f6de52a268ee --- /dev/null +++ b/site/e2e/pom/TemplatesPage.ts @@ -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() + } +} diff --git a/site/e2e/pom/WorkspacePage.ts b/site/e2e/pom/WorkspacePage.ts new file mode 100644 index 0000000000000..af84c3ba355c2 --- /dev/null +++ b/site/e2e/pom/WorkspacePage.ts @@ -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" }) + } +} diff --git a/site/e2e/pom/index.ts b/site/e2e/pom/index.ts deleted file mode 100644 index b050895f83720..0000000000000 --- a/site/e2e/pom/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./SignInPage" -export * from "./WorkspacesPage" diff --git a/site/e2e/states/.gitkeep b/site/e2e/states/.gitkeep deleted file mode 100644 index e69de29bb2d1d..0000000000000 diff --git a/site/e2e/testdata/docker.tar b/site/e2e/testdata/docker.tar new file mode 100644 index 0000000000000..54d015ced3107 Binary files /dev/null and b/site/e2e/testdata/docker.tar differ diff --git a/site/e2e/tests/basicScenario.spec.ts b/site/e2e/tests/basicScenario.spec.ts new file mode 100644 index 0000000000000..ce636bae639ff --- /dev/null +++ b/site/e2e/tests/basicScenario.spec.ts @@ -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 + }) +}) diff --git a/site/e2e/tests/listTemplates.spec.ts b/site/e2e/tests/listTemplates.spec.ts deleted file mode 100644 index 4d3569bfb47f3..0000000000000 --- a/site/e2e/tests/listTemplates.spec.ts +++ /dev/null @@ -1,9 +0,0 @@ -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" }) - await expect(page).toHaveTitle("Templates - Coder") -}) 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/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx index 326239604730c..68d7d65f571c1 100644 --- a/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx +++ b/site/src/components/Dialogs/DeleteDialog/DeleteDialog.tsx @@ -57,6 +57,7 @@ export const DeleteDialog: FC> = ({ label={t("deleteDialog.confirmLabel", { entity })} error={hasError} helperText={hasError && t("deleteDialog.incorrectName", { entity })} + data-testid="delete-dialog-confirmation" /> ) diff --git a/site/src/components/Dialogs/Dialog.tsx b/site/src/components/Dialogs/Dialog.tsx index 7f249c74c590b..9073f3d495a57 100644 --- a/site/src/components/Dialogs/Dialog.tsx +++ b/site/src/components/Dialogs/Dialog.tsx @@ -75,6 +75,7 @@ export const DialogActionButtons: React.FC = ({ [styles.errorButton]: type === "delete", [styles.successButton]: type === "success", })} + data-testid={type + "-dialog-confirm"} > {confirmText} diff --git a/site/src/components/FileUpload/FileUpload.tsx b/site/src/components/FileUpload/FileUpload.tsx index 229aa2af02c9d..96a7a85a0b199 100644 --- a/site/src/components/FileUpload/FileUpload.tsx +++ b/site/src/components/FileUpload/FileUpload.tsx @@ -120,6 +120,7 @@ export const FileUpload: FC = ({ ref={inputRef} className={styles.input} accept={extension} + data-testid="file-upload" onChange={(event) => { const file = event.currentTarget.files?.[0] if (file) { diff --git a/site/src/components/FormFooter/FormFooter.tsx b/site/src/components/FormFooter/FormFooter.tsx index 804f3ef791546..2939faaec8977 100644 --- a/site/src/components/FormFooter/FormFooter.tsx +++ b/site/src/components/FormFooter/FormFooter.tsx @@ -16,6 +16,7 @@ export interface FormFooterProps { styles?: FormFooterStyles submitLabel?: string submitDisabled?: boolean + submitTestId?: string } export const FormFooter: FC = ({ @@ -23,6 +24,7 @@ export const FormFooter: FC = ({ isLoading, submitDisabled, submitLabel = Language.defaultSubmitLabel, + submitTestId, styles = defaultStyles(), }) => { return ( @@ -35,6 +37,7 @@ export const FormFooter: FC = ({ color="primary" type="submit" disabled={submitDisabled} + data-testid={submitTestId} > {submitLabel} diff --git a/site/src/components/Pill/Pill.tsx b/site/src/components/Pill/Pill.tsx index d812ea2b66e1d..e872df04b3d4d 100644 --- a/site/src/components/Pill/Pill.tsx +++ b/site/src/components/Pill/Pill.tsx @@ -10,16 +10,18 @@ export interface PillProps { type?: PaletteIndex lightBorder?: boolean title?: string + dataTestId?: string } export const Pill: FC = (props) => { - const { className, icon, text = false, title } = props + const { className, icon, text = false, title, dataTestId } = props const styles = useStyles(props) return (
{icon &&
{icon}
} {text} diff --git a/site/src/components/Resources/AgentStatus.tsx b/site/src/components/Resources/AgentStatus.tsx index d03fb1c664e32..89c833add5ce8 100644 --- a/site/src/components/Resources/AgentStatus.tsx +++ b/site/src/components/Resources/AgentStatus.tsx @@ -28,6 +28,7 @@ const ReadyLifecycle: React.FC = () => { role="status" aria-label={t("agentStatus.connected.ready")} className={combineClasses([styles.status, styles.connected])} + data-testid="agent-lifecycle-ready" /> ) } diff --git a/site/src/components/Resources/AgentVersion.tsx b/site/src/components/Resources/AgentVersion.tsx index 713609b686197..efb72cc14cbc6 100644 --- a/site/src/components/Resources/AgentVersion.tsx +++ b/site/src/components/Resources/AgentVersion.tsx @@ -16,7 +16,7 @@ export const AgentVersion: FC<{ const { outdated } = getDisplayVersionStatus(agent.version, serverVersion) if (!outdated) { - return Updated + return Updated } return ( @@ -28,6 +28,7 @@ export const AgentVersion: FC<{ onMouseEnter={() => setIsOpen(true)} onMouseLeave={() => setIsOpen(false)} className={styles.trigger} + data-testid="agent-version" > Outdated diff --git a/site/src/components/TemplateLayout/TemplatePageHeader.tsx b/site/src/components/TemplateLayout/TemplatePageHeader.tsx index ef978ca99e511..cc61a5c96c539 100644 --- a/site/src/components/TemplateLayout/TemplatePageHeader.tsx +++ b/site/src/components/TemplateLayout/TemplatePageHeader.tsx @@ -103,6 +103,7 @@ const CreateWorkspaceButton: FC<{ startIcon={} component={RouterLink} to={`/templates/${templateName}/workspace`} + data-testid="button-create-workspace" > Create workspace diff --git a/site/src/components/TerminalLink/TerminalLink.tsx b/site/src/components/TerminalLink/TerminalLink.tsx index 05d51d31e924b..76c72e2667309 100644 --- a/site/src/components/TerminalLink/TerminalLink.tsx +++ b/site/src/components/TerminalLink/TerminalLink.tsx @@ -36,6 +36,7 @@ export const TerminalLink: FC> = ({ underline="none" href={href} target="_blank" + data-testid="button-terminal" onClick={(event) => { event.preventDefault() window.open( diff --git a/site/src/components/WorkspaceActions/Buttons.tsx b/site/src/components/WorkspaceActions/Buttons.tsx index b8c38469df68f..b982fa4ff9416 100644 --- a/site/src/components/WorkspaceActions/Buttons.tsx +++ b/site/src/components/WorkspaceActions/Buttons.tsx @@ -60,6 +60,7 @@ export const StopButton: FC> = ({ startIcon={} onClick={handleAction} className={styles.fixedWidth} + data-testid="button-stop-workspace" > {t("actionButton.stop")} diff --git a/site/src/components/WorkspaceActions/WorkspaceActions.tsx b/site/src/components/WorkspaceActions/WorkspaceActions.tsx index d7508ed64405b..8cdbed837d2b9 100644 --- a/site/src/components/WorkspaceActions/WorkspaceActions.tsx +++ b/site/src/components/WorkspaceActions/WorkspaceActions.tsx @@ -159,7 +159,10 @@ export const WorkspaceActions: FC = ({ Change version )} - + Delete diff --git a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx index 0a23ac7409bfa..97ddebc1100b8 100644 --- a/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx +++ b/site/src/components/WorkspaceStatusBadge/WorkspaceStatusBadge.tsx @@ -100,5 +100,13 @@ export const WorkspaceStatusBadge: FC< PropsWithChildren > = ({ build, className }) => { const { text, icon, type } = getStatus(build.status) - return + return ( + + ) } diff --git a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx index da6dcea1c2533..5c7de16d88cff 100644 --- a/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx +++ b/site/src/pages/CreateTemplatePage/CreateTemplateForm.tsx @@ -225,7 +225,10 @@ export const CreateTemplateForm: FC = ({ const { t: commonT } = useTranslation("common") return ( - + {/* General info */} = ({ onCancel={onCancel} isLoading={isSubmitting} submitLabel={jobError ? "Retry" : "Create template"} + submitTestId="button-create-template" /> ) diff --git a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx index ba5a7e38eba6c..12e6cd3f41144 100644 --- a/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx +++ b/site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx @@ -162,7 +162,10 @@ export const CreateWorkspacePageView: FC< return ( - + {Boolean(props.hasTemplateErrors) && ( {Boolean( @@ -393,6 +396,7 @@ export const CreateWorkspacePageView: FC< onCancel={props.onCancel} isLoading={props.creatingWorkspace} submitLabel={t("createWorkspace")} + submitTestId="button-create-workspace" /> diff --git a/site/src/pages/TemplatesPage/TemplatesPageView.tsx b/site/src/pages/TemplatesPage/TemplatesPageView.tsx index de63ab467ce6e..a22691ea89569 100644 --- a/site/src/pages/TemplatesPage/TemplatesPageView.tsx +++ b/site/src/pages/TemplatesPage/TemplatesPageView.tsx @@ -158,7 +158,12 @@ export const TemplatesPageView: FC< > Starter templates -