Skip to content

Commit a2eec18

Browse files
committed
Add tests
1 parent 3513404 commit a2eec18

File tree

9 files changed

+243
-31
lines changed

9 files changed

+243
-31
lines changed

site/src/api/api.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -377,7 +377,7 @@ export const patchTemplateVersion = async (
377377
templateVersionId: string,
378378
data: TypesGen.PatchTemplateVersionRequest,
379379
) => {
380-
const response = await axios.patch<Types.Message>(
380+
const response = await axios.patch<TypesGen.TemplateVersion>(
381381
`/api/v2/templateversions/${templateVersionId}`,
382382
data,
383383
)
@@ -1015,3 +1015,26 @@ const getMissingParameters = (
10151015

10161016
return missingParameters
10171017
}
1018+
1019+
export const watchBuildLogs = (
1020+
versionId: string,
1021+
onMessage: (log: TypesGen.ProvisionerJobLog) => void,
1022+
) => {
1023+
return new Promise<void>((resolve, reject) => {
1024+
const proto = location.protocol === "https:" ? "wss:" : "ws:"
1025+
const socket = new WebSocket(
1026+
`${proto}//${location.host}/api/v2/templateversions/${versionId}/logs?follow=true`,
1027+
)
1028+
socket.binaryType = "blob"
1029+
socket.addEventListener("message", (event) =>
1030+
onMessage(JSON.parse(event.data) as TypesGen.ProvisionerJobLog),
1031+
)
1032+
socket.addEventListener("error", () => {
1033+
reject(new Error("Connection for logs failed."))
1034+
})
1035+
socket.addEventListener("close", () => {
1036+
// When the socket closes, logs have finished streaming!
1037+
resolve()
1038+
})
1039+
})
1040+
}

site/src/components/TemplateVersionEditor/PublishTemplateVersionDialog.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DialogProps } from "components/Dialogs/Dialog"
22
import { FC } from "react"
3-
import { getFormHelpers, nameValidator } from "util/formUtils"
3+
import { getFormHelpers } from "util/formUtils"
44
import { FormFields } from "components/Form/Form"
55
import { useFormik } from "formik"
66
import * as Yup from "yup"
@@ -12,21 +12,22 @@ import FormControlLabel from "@material-ui/core/FormControlLabel"
1212
import { Stack } from "components/Stack/Stack"
1313

1414
export type PublishTemplateVersionDialogProps = DialogProps & {
15+
defaultName: string
1516
isPublishing: boolean
1617
onClose: () => void
1718
onConfirm: (data: PublishVersionData) => void
1819
}
1920

2021
export const PublishTemplateVersionDialog: FC<
2122
PublishTemplateVersionDialogProps
22-
> = ({ onConfirm, isPublishing, onClose, ...dialogProps }) => {
23+
> = ({ onConfirm, isPublishing, onClose, defaultName, ...dialogProps }) => {
2324
const form = useFormik({
2425
initialValues: {
25-
name: "",
26+
name: defaultName,
2627
isActiveVersion: false,
2728
},
2829
validationSchema: Yup.object({
29-
name: nameValidator("name").optional(),
30+
name: Yup.string().required(),
3031
isActiveVersion: Yup.boolean(),
3132
}),
3233
onSubmit: onConfirm,
@@ -62,7 +63,6 @@ export const PublishTemplateVersionDialog: FC<
6263
InputLabelProps={{
6364
shrink: true,
6465
}}
65-
helperText="If you leave this blank, the version name will be automatically generated."
6666
/>
6767

6868
<FormControlLabel

site/src/components/TemplateVersionEditor/TemplateVersionEditor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
168168
return (
169169
<>
170170
<div className={styles.root}>
171-
<div className={styles.topbar}>
171+
<div className={styles.topbar} data-testid="topbar">
172172
<div className={styles.topbarSides}>
173173
<AvatarData
174174
title={template.display_name || template.name}
@@ -404,6 +404,7 @@ export const TemplateVersionEditor: FC<TemplateVersionEditorProps> = ({
404404
onClose={onCancelPublish}
405405
onConfirm={onConfirmPublish}
406406
isPublishing={isPublishing}
407+
defaultName={templateVersion.name}
407408
/>
408409
</>
409410
)
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import {
2+
MockTemplateVersion,
3+
MockWorkspaceBuildLogs,
4+
renderWithAuth,
5+
} from "testHelpers/renderHelpers"
6+
import TemplateVersionEditorPage from "./TemplateVersionEditorPage"
7+
import { screen, waitFor, within } from "@testing-library/react"
8+
import userEvent from "@testing-library/user-event"
9+
import * as api from "api/api"
10+
11+
// For some reason this component in Jest is throwing a MUI style warning so,
12+
// since we don't need it for this test, we can mock it out
13+
jest.mock("components/TemplateResourcesTable/TemplateResourcesTable", () => {
14+
return {
15+
TemplateResourcesTable: () => <div />,
16+
}
17+
})
18+
19+
test("Use custom name and set it as active when publishing", async () => {
20+
const user = userEvent.setup()
21+
renderWithAuth(<TemplateVersionEditorPage />, {
22+
extraRoutes: [
23+
{
24+
path: "/templates/:templateId",
25+
element: <div />,
26+
},
27+
],
28+
})
29+
const topbar = await screen.findByTestId("topbar")
30+
31+
// Build Template
32+
jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" })
33+
jest
34+
.spyOn(api, "createTemplateVersion")
35+
.mockResolvedValueOnce(MockTemplateVersion)
36+
jest
37+
.spyOn(api, "getTemplateVersion")
38+
.mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" })
39+
jest.spyOn(api, "watchBuildLogs").mockImplementation((_, onMessage) => {
40+
onMessage(MockWorkspaceBuildLogs[0])
41+
return Promise.resolve()
42+
})
43+
const buildButton = within(topbar).getByRole("button", {
44+
name: "Build template",
45+
})
46+
await user.click(buildButton)
47+
48+
// Publish
49+
const patchTemplateVersion = jest
50+
.spyOn(api, "patchTemplateVersion")
51+
.mockResolvedValue(MockTemplateVersion)
52+
const updateActiveTemplateVersion = jest
53+
.spyOn(api, "updateActiveTemplateVersion")
54+
.mockResolvedValue({ message: "" })
55+
await within(topbar).findByText("Success")
56+
const publishButton = within(topbar).getByRole("button", {
57+
name: "Publish version",
58+
})
59+
await user.click(publishButton)
60+
const publishDialog = await screen.findByTestId("dialog")
61+
const nameField = within(publishDialog).getByLabelText("Version name")
62+
await user.clear(nameField)
63+
await user.type(nameField, "v1.0")
64+
await user.click(
65+
within(publishDialog).getByLabelText("Promote to default version"),
66+
)
67+
await user.click(
68+
within(publishDialog).getByRole("button", { name: "Publish" }),
69+
)
70+
await waitFor(() => {
71+
expect(patchTemplateVersion).toBeCalledWith("new-version-id", {
72+
name: "v1.0",
73+
})
74+
expect(updateActiveTemplateVersion).toBeCalledWith("test-template", {
75+
id: "new-version-id",
76+
})
77+
})
78+
})
79+
80+
test("Do not mark as active if promote is not checked", async () => {
81+
const user = userEvent.setup()
82+
renderWithAuth(<TemplateVersionEditorPage />, {
83+
extraRoutes: [
84+
{
85+
path: "/templates/:templateId",
86+
element: <div />,
87+
},
88+
],
89+
})
90+
const topbar = await screen.findByTestId("topbar")
91+
92+
// Build Template
93+
jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" })
94+
jest
95+
.spyOn(api, "createTemplateVersion")
96+
.mockResolvedValueOnce(MockTemplateVersion)
97+
jest
98+
.spyOn(api, "getTemplateVersion")
99+
.mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" })
100+
jest.spyOn(api, "watchBuildLogs").mockImplementation((_, onMessage) => {
101+
onMessage(MockWorkspaceBuildLogs[0])
102+
return Promise.resolve()
103+
})
104+
const buildButton = within(topbar).getByRole("button", {
105+
name: "Build template",
106+
})
107+
await user.click(buildButton)
108+
109+
// Publish
110+
const patchTemplateVersion = jest
111+
.spyOn(api, "patchTemplateVersion")
112+
.mockResolvedValue(MockTemplateVersion)
113+
const updateActiveTemplateVersion = jest
114+
.spyOn(api, "updateActiveTemplateVersion")
115+
.mockResolvedValue({ message: "" })
116+
await within(topbar).findByText("Success")
117+
const publishButton = within(topbar).getByRole("button", {
118+
name: "Publish version",
119+
})
120+
await user.click(publishButton)
121+
const publishDialog = await screen.findByTestId("dialog")
122+
const nameField = within(publishDialog).getByLabelText("Version name")
123+
await user.clear(nameField)
124+
await user.type(nameField, "v1.0")
125+
await user.click(
126+
within(publishDialog).getByRole("button", { name: "Publish" }),
127+
)
128+
await waitFor(() => {
129+
expect(patchTemplateVersion).toBeCalledWith("new-version-id", {
130+
name: "v1.0",
131+
})
132+
})
133+
expect(updateActiveTemplateVersion).toBeCalledTimes(0)
134+
})
135+
136+
test("The default version name is used when a new one is not used", async () => {
137+
const user = userEvent.setup()
138+
renderWithAuth(<TemplateVersionEditorPage />, {
139+
extraRoutes: [
140+
{
141+
path: "/templates/:templateId",
142+
element: <div />,
143+
},
144+
],
145+
})
146+
const topbar = await screen.findByTestId("topbar")
147+
148+
// Build Template
149+
jest.spyOn(api, "uploadTemplateFile").mockResolvedValueOnce({ hash: "hash" })
150+
jest
151+
.spyOn(api, "createTemplateVersion")
152+
.mockResolvedValueOnce(MockTemplateVersion)
153+
jest
154+
.spyOn(api, "getTemplateVersion")
155+
.mockResolvedValue({ ...MockTemplateVersion, id: "new-version-id" })
156+
jest.spyOn(api, "watchBuildLogs").mockImplementation((_, onMessage) => {
157+
onMessage(MockWorkspaceBuildLogs[0])
158+
return Promise.resolve()
159+
})
160+
const buildButton = within(topbar).getByRole("button", {
161+
name: "Build template",
162+
})
163+
await user.click(buildButton)
164+
165+
// Publish
166+
const patchTemplateVersion = jest
167+
.spyOn(api, "patchTemplateVersion")
168+
.mockResolvedValue(MockTemplateVersion)
169+
await within(topbar).findByText("Success")
170+
const publishButton = within(topbar).getByRole("button", {
171+
name: "Publish version",
172+
})
173+
await user.click(publishButton)
174+
const publishDialog = await screen.findByTestId("dialog")
175+
await user.click(
176+
within(publishDialog).getByRole("button", { name: "Publish" }),
177+
)
178+
await waitFor(() => {
179+
expect(patchTemplateVersion).toBeCalledWith("new-version-id", {
180+
name: MockTemplateVersion.name,
181+
})
182+
})
183+
})
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
export type PublishVersionData = {
2-
name?: string
2+
name: string
33
isActiveVersion: boolean
44
}

site/src/testHelpers/entities.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -657,9 +657,10 @@ export const MockWorkspace: TypesGen.Workspace = {
657657
owner_id: MockUser.id,
658658
owner_name: MockUser.username,
659659
autostart_schedule: MockWorkspaceAutostartEnabled.schedule,
660-
ttl_ms: 2 * 60 * 60 * 1000, // 2 hours as milliseconds
660+
ttl_ms: 2 * 60 * 60 * 1000,
661661
latest_build: MockWorkspaceBuild,
662-
last_used_at: "",
662+
last_used_at: "2022-05-16T15:29:10.302441433Z",
663+
organization_id: MockOrganization.id,
663664
}
664665

665666
export const MockStoppedWorkspace: TypesGen.Workspace = {

site/src/testHelpers/handlers.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import { CreateWorkspaceBuildRequest } from "../api/typesGenerated"
44
import { permissionsToCheck } from "../xServices/auth/authXService"
55
import * as M from "./entities"
66
import { MockGroup, MockWorkspaceQuota } from "./entities"
7+
import fs from "fs"
8+
import path from "path"
79

810
export const handlers = [
911
rest.get("/api/v2/templates/:templateId/daus", async (req, res, ctx) => {
@@ -318,4 +320,17 @@ export const handlers = [
318320
return res(ctx.status(200), ctx.json([M.MockWorkspaceBuildParameter1]))
319321
},
320322
),
323+
324+
rest.get("api/v2/files/:fileId", (_, res, ctx) => {
325+
const fileBuffer = fs.readFileSync(
326+
path.resolve(__dirname, "./templateFiles.tar"),
327+
)
328+
329+
return res(
330+
ctx.set("Content-Length", fileBuffer.byteLength.toString()),
331+
ctx.set("Content-Type", "application/octet-stream"),
332+
// Respond with the "ArrayBuffer".
333+
ctx.body(fileBuffer),
334+
)
335+
}),
321336
]
27 KB
Binary file not shown.

site/src/xServices/templateVersionEditor/templateVersionEditorXService.ts

Lines changed: 10 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -295,28 +295,17 @@ export const templateVersionEditorMachine = createMachine(
295295
}
296296
return API.getTemplateVersion(ctx.version.id)
297297
},
298-
watchBuildLogs: (ctx) => async (callback) => {
299-
return new Promise<void>((resolve, reject) => {
300-
if (!ctx.version) {
301-
return reject("version must be set")
298+
watchBuildLogs:
299+
({ version }) =>
300+
async (callback) => {
301+
if (!version) {
302+
throw new Error("version must be set")
302303
}
303-
const proto = location.protocol === "https:" ? "wss:" : "ws:"
304-
const socket = new WebSocket(
305-
`${proto}//${location.host}/api/v2/templateversions/${ctx.version?.id}/logs?follow=true`,
306-
)
307-
socket.binaryType = "blob"
308-
socket.addEventListener("message", (event) => {
309-
callback({ type: "ADD_BUILD_LOG", log: JSON.parse(event.data) })
310-
})
311-
socket.addEventListener("error", () => {
312-
reject(new Error("socket errored"))
313-
})
314-
socket.addEventListener("close", () => {
315-
// When the socket closes, logs have finished streaming!
316-
resolve()
304+
305+
return API.watchBuildLogs(version.id, (log) => {
306+
callback({ type: "ADD_BUILD_LOG", log })
317307
})
318-
})
319-
},
308+
},
320309
getResources: (ctx) => {
321310
if (!ctx.version) {
322311
throw new Error("template version must be set")
@@ -342,7 +331,7 @@ export const templateVersionEditorMachine = createMachine(
342331
throw new Error("Template is not set")
343332
}
344333
await Promise.all([
345-
API.patchTemplateVersion(version.id, { name: name ?? version.name }),
334+
API.patchTemplateVersion(version.id, { name }),
346335
isActiveVersion
347336
? API.updateActiveTemplateVersion(templateId, {
348337
id: version.id,

0 commit comments

Comments
 (0)