mirror of https://github.com/coder/coder.git
chore(site): Make FE tests faster (#6543)
This commit is contained in:
parent
9b2abf0952
commit
813b54942f
|
@ -512,7 +512,7 @@ jobs:
|
|||
- name: Install node_modules
|
||||
run: ./scripts/yarn_install.sh
|
||||
|
||||
- run: yarn test:ci
|
||||
- run: yarn test:ci --max-workers ${{ steps.cpu-cores.outputs.count }}
|
||||
working-directory: site
|
||||
|
||||
- uses: codecov/codecov-action@v3
|
||||
|
|
|
@ -27,6 +27,7 @@ site/test-results/*
|
|||
site/e2e/test-results/*
|
||||
site/e2e/states/*.json
|
||||
site/playwright-report/*
|
||||
site/.swc
|
||||
|
||||
# Make target for updating golden files.
|
||||
cli/testdata/.gen-golden
|
||||
|
|
|
@ -30,6 +30,7 @@ site/test-results/*
|
|||
site/e2e/test-results/*
|
||||
site/e2e/states/*.json
|
||||
site/playwright-report/*
|
||||
site/.swc
|
||||
|
||||
# Make target for updating golden files.
|
||||
cli/testdata/.gen-golden
|
||||
|
|
|
@ -30,6 +30,7 @@ test-results/*
|
|||
e2e/test-results/*
|
||||
e2e/states/*.json
|
||||
playwright-report/*
|
||||
.swc
|
||||
|
||||
# Make target for updating golden files.
|
||||
../cli/testdata/.gen-golden
|
||||
|
|
|
@ -30,6 +30,7 @@ test-results/*
|
|||
e2e/test-results/*
|
||||
e2e/states/*.json
|
||||
playwright-report/*
|
||||
.swc
|
||||
|
||||
# Make target for updating golden files.
|
||||
../cli/testdata/.gen-golden
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
// REMARK: Jest is supposed to never exceed 50% maxWorkers by default. However,
|
||||
// there seems to be an issue with this in our Ubuntu-based workspaces.
|
||||
// If we don't limit it, then 100% CPU and high MEM usage is hit
|
||||
// unexpectedly, leading to OOM kills.
|
||||
//
|
||||
// SEE thread: https://github.com/coder/coder/pull/483#discussion_r829636583
|
||||
const maxWorkers = 2
|
||||
|
||||
module.exports = {
|
||||
maxWorkers,
|
||||
testTimeout: 10_000,
|
||||
maxWorkers: 8,
|
||||
projects: [
|
||||
{
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "./tsconfig.test.json",
|
||||
},
|
||||
},
|
||||
coverageReporters: ["text", "lcov"],
|
||||
displayName: "test",
|
||||
preset: "ts-jest",
|
||||
roots: ["<rootDir>"],
|
||||
setupFilesAfterEnv: ["./jest.setup.ts"],
|
||||
extensionsToTreatAsEsm: [".ts"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
"\\.m?jsx?$": "jest-esm-transformer",
|
||||
"^.+\\.(t|j)sx?$": [
|
||||
"@swc/jest",
|
||||
{
|
||||
jsc: {
|
||||
transform: {
|
||||
react: {
|
||||
runtime: "automatic",
|
||||
},
|
||||
},
|
||||
experimental: {
|
||||
plugins: [["jest_workaround", {}]],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: "jsdom",
|
||||
testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.tsx?$",
|
||||
|
|
|
@ -5,7 +5,9 @@ import { server } from "./src/testHelpers/server"
|
|||
import "jest-location-mock"
|
||||
import { TextEncoder, TextDecoder } from "util"
|
||||
import { Blob } from "buffer"
|
||||
import { fetch, Request, Response, Headers } from "@remix-run/web-fetch"
|
||||
import jestFetchMock from "jest-fetch-mock"
|
||||
|
||||
jestFetchMock.enableMocks()
|
||||
|
||||
global.TextEncoder = TextEncoder
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||
|
@ -13,22 +15,6 @@ global.TextDecoder = TextDecoder as any
|
|||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Polyfill for jsdom
|
||||
global.Blob = Blob as any
|
||||
|
||||
// From REMIX https://github.com/remix-run/react-router/blob/main/packages/react-router-dom/__tests__/setup.ts
|
||||
if (!global.fetch) {
|
||||
// Built-in lib.dom.d.ts expects `fetch(Request | string, ...)` but the web
|
||||
// fetch API allows a URL so @remix-run/web-fetch defines
|
||||
// `fetch(string | URL | Request, ...)`
|
||||
// @ts-expect-error -- Polyfill for jsdom
|
||||
global.fetch = fetch
|
||||
// Same as above, lib.dom.d.ts doesn't allow a URL to the Request constructor
|
||||
// @ts-expect-error -- Polyfill for jsdom
|
||||
global.Request = Request
|
||||
// web-std/fetch Response does not currently implement Response.error()
|
||||
// @ts-expect-error -- Polyfill for jsdom
|
||||
global.Response = Response
|
||||
global.Headers = Headers
|
||||
}
|
||||
|
||||
// Polyfill the getRandomValues that is used on utils/random.ts
|
||||
Object.defineProperty(global.self, "crypto", {
|
||||
value: {
|
||||
|
|
|
@ -36,7 +36,6 @@
|
|||
"@material-ui/icons": "4.5.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.42",
|
||||
"@monaco-editor/react": "4.4.6",
|
||||
"@remix-run/web-fetch": "4.3.2",
|
||||
"@tanstack/react-query": "4.22.4",
|
||||
"@testing-library/react-hooks": "8.0.1",
|
||||
"@types/color-convert": "2.0.0",
|
||||
|
@ -59,6 +58,7 @@
|
|||
"front-matter": "4.0.2",
|
||||
"history": "5.3.0",
|
||||
"i18next": "21.9.1",
|
||||
"jest-environment-jsdom": "29.5.0",
|
||||
"jest-location-mock": "1.0.9",
|
||||
"just-debounce-it": "3.1.1",
|
||||
"lodash": "4.17.21",
|
||||
|
@ -93,10 +93,12 @@
|
|||
"@storybook/addon-essentials": "6.5.12",
|
||||
"@storybook/addon-links": "6.5.9",
|
||||
"@storybook/react": "6.5.12",
|
||||
"@swc/core": "1.3.38",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@testing-library/jest-dom": "5.16.4",
|
||||
"@testing-library/react": "13.4.0",
|
||||
"@testing-library/user-event": "14.4.3",
|
||||
"@types/jest": "27.4.1",
|
||||
"@types/jest": "29.4.0",
|
||||
"@types/node": "14.18.22",
|
||||
"@types/react": "18.0.15",
|
||||
"@types/react-dom": "18.0.6",
|
||||
|
@ -120,17 +122,17 @@
|
|||
"eslint-plugin-react": "7.31.1",
|
||||
"eslint-plugin-react-hooks": "4.6.0",
|
||||
"eslint-plugin-unicorn": "44.0.0",
|
||||
"jest": "27.5.1",
|
||||
"jest": "29.5.0",
|
||||
"jest-canvas-mock": "2.4.0",
|
||||
"jest-esm-transformer": "1.0.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-runner-eslint": "1.1.0",
|
||||
"jest-websocket-mock": "2.4.0",
|
||||
"jest_workaround": "0.1.14",
|
||||
"monaco-editor": "0.34.1",
|
||||
"msw": "0.47.0",
|
||||
"msw": "1.1.0",
|
||||
"prettier": "2.8.1",
|
||||
"resize-observer": "1.0.4",
|
||||
"semver": "7.3.7",
|
||||
"ts-jest": "27.1.4",
|
||||
"typescript": "4.8.2"
|
||||
},
|
||||
"browserslist": [
|
||||
|
|
|
@ -192,7 +192,6 @@ export const DeploymentBannerView: FC<DeploymentBannerViewProps> = ({
|
|||
<Tooltip title="A countdown until stats are fetched again. Click to refresh!">
|
||||
<Button
|
||||
className={`${styles.value} ${styles.refreshButton}`}
|
||||
title="Refresh"
|
||||
onClick={() => {
|
||||
if (fetchStats) {
|
||||
fetchStats()
|
||||
|
|
|
@ -123,7 +123,12 @@ export const ConfirmDialog: FC<PropsWithChildren<ConfirmDialogProps>> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<Dialog className={styles.dialogWrapper} onClose={onClose} open={open}>
|
||||
<Dialog
|
||||
className={styles.dialogWrapper}
|
||||
onClose={onClose}
|
||||
open={open}
|
||||
data-testid="dialog"
|
||||
>
|
||||
<div className={styles.dialogContent}>
|
||||
<h3 className={styles.dialogTitle}>{title}</h3>
|
||||
{description && (
|
||||
|
|
|
@ -30,7 +30,7 @@ export const DropdownButton: FC<DropdownButtonProps> = ({
|
|||
const canOpen = secondaryActions.length > 0
|
||||
|
||||
return (
|
||||
<span className={styles.buttonContainer}>
|
||||
<span className={styles.buttonContainer} data-testid="workspace-actions">
|
||||
{/* primary workspace CTA */}
|
||||
<span data-testid="primary-cta" className={styles.primaryCta}>
|
||||
{primaryAction}
|
||||
|
|
|
@ -20,7 +20,7 @@ export const FullScreenLoader: FC = () => {
|
|||
const styles = useStyles()
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.root} data-testid="loader">
|
||||
<CircularProgress />
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -12,6 +12,7 @@ export const Loader: FC<React.PropsWithChildren<{ size?: number }>> = ({
|
|||
display="flex"
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
data-testid="loader"
|
||||
>
|
||||
<CircularProgress size={size} />
|
||||
</Box>
|
||||
|
|
|
@ -7,12 +7,10 @@ import {
|
|||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
MockTemplateVersionVariable3,
|
||||
MockTemplateVersionVariable4,
|
||||
MockTemplateVersionVariable5,
|
||||
renderWithAuth,
|
||||
} from "testHelpers/renderHelpers"
|
||||
import CreateTemplatePage from "./CreateTemplatePage"
|
||||
import { screen, waitFor } from "@testing-library/react"
|
||||
import { screen, waitFor, within } from "@testing-library/react"
|
||||
import userEvent from "@testing-library/user-event"
|
||||
import * as API from "api/api"
|
||||
|
||||
|
@ -55,19 +53,19 @@ test("Create template with variables", async () => {
|
|||
MockTemplateVersionVariable1,
|
||||
MockTemplateVersionVariable2,
|
||||
MockTemplateVersionVariable3,
|
||||
MockTemplateVersionVariable4,
|
||||
MockTemplateVersionVariable5,
|
||||
])
|
||||
|
||||
// Render page, fill the name and submit
|
||||
const { router } = await renderPage()
|
||||
const { router, container } = await renderPage()
|
||||
const form = container.querySelector("form") as HTMLFormElement
|
||||
await userEvent.type(screen.getByLabelText(/Name/), "my-template")
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: /create template/i }),
|
||||
within(form).getByRole("button", { name: /create template/i }),
|
||||
)
|
||||
|
||||
// Wait for the variables form to be rendered and fill it
|
||||
await screen.findByText(/Variables/)
|
||||
|
||||
// Type first variable
|
||||
await userEvent.clear(screen.getByLabelText(/var.first_variable/))
|
||||
await userEvent.type(
|
||||
|
@ -79,18 +77,6 @@ test("Create template with variables", async () => {
|
|||
await userEvent.type(screen.getByLabelText(/var.second_variable/), "2")
|
||||
// Select third variable on radio
|
||||
await userEvent.click(screen.getByLabelText(/True/))
|
||||
// Type fourth variable
|
||||
await userEvent.clear(screen.getByLabelText(/var.fourth_variable/))
|
||||
await userEvent.type(
|
||||
screen.getByLabelText(/var.fourth_variable/),
|
||||
"Fourth value",
|
||||
)
|
||||
// Type fifth variable
|
||||
await userEvent.clear(screen.getByLabelText(/var.fifth_variable/))
|
||||
await userEvent.type(
|
||||
screen.getByLabelText(/var.fifth_variable/),
|
||||
"Fifth value",
|
||||
)
|
||||
// Setup the mock for the second template version creation before submit the form
|
||||
jest.clearAllMocks()
|
||||
jest
|
||||
|
@ -98,9 +84,8 @@ test("Create template with variables", async () => {
|
|||
.mockResolvedValue(MockTemplateVersion)
|
||||
jest.spyOn(API, "createTemplate").mockResolvedValue(MockTemplate)
|
||||
await userEvent.click(
|
||||
screen.getByRole("button", { name: /create template/i }),
|
||||
within(form).getByRole("button", { name: /create template/i }),
|
||||
)
|
||||
|
||||
await waitFor(() => expect(API.createTemplate).toBeCalledTimes(1))
|
||||
expect(router.state.location.pathname).toEqual(
|
||||
`/templates/${MockTemplate.name}`,
|
||||
|
@ -115,8 +100,6 @@ test("Create template with variables", async () => {
|
|||
{ name: "first_variable", value: "First value" },
|
||||
{ name: "second_variable", value: "2" },
|
||||
{ name: "third_variable", value: "true" },
|
||||
{ name: "fourth_variable", value: "Fourth value" },
|
||||
{ name: "fifth_variable", value: "Fifth value" },
|
||||
],
|
||||
})
|
||||
})
|
||||
|
|
|
@ -35,6 +35,7 @@ const renderWorkspacePage = async () => {
|
|||
route: `/@${MockWorkspace.owner_name}/${MockWorkspace.name}`,
|
||||
path: "/@:username/:workspace",
|
||||
})
|
||||
|
||||
await waitForLoaderToBeRemoved()
|
||||
}
|
||||
|
||||
|
@ -46,9 +47,9 @@ const renderWorkspacePage = async () => {
|
|||
*/
|
||||
const testButton = async (label: string, actionMock: jest.SpyInstance) => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
await renderWorkspacePage()
|
||||
const button = await screen.findByRole("button", { name: label })
|
||||
const workspaceActions = screen.getByTestId("workspace-actions")
|
||||
const button = within(workspaceActions).getByRole("button", { name: label })
|
||||
await user.click(button)
|
||||
expect(actionMock).toBeCalled()
|
||||
}
|
||||
|
@ -86,32 +87,36 @@ afterAll(() => {
|
|||
|
||||
describe("WorkspacePage", () => {
|
||||
it("requests a delete job when the user presses Delete and confirms", async () => {
|
||||
const user = userEvent.setup()
|
||||
|
||||
const user = userEvent.setup({ delay: 0 })
|
||||
const deleteWorkspaceMock = jest
|
||||
.spyOn(api, "deleteWorkspace")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild)
|
||||
await renderWorkspacePage()
|
||||
|
||||
// open the workspace action popover so we have access to all available ctas
|
||||
const trigger = await screen.findByTestId("workspace-actions-button")
|
||||
const trigger = screen.getByTestId("workspace-actions-button")
|
||||
await user.click(trigger)
|
||||
|
||||
const buttonText = t("actionButton.delete", { ns: "workspacePage" })
|
||||
|
||||
// Click on delete
|
||||
const button = await screen.findByText(buttonText)
|
||||
await user.click(button)
|
||||
|
||||
// Get dialog and confirm
|
||||
const dialog = await screen.findByTestId("dialog")
|
||||
const labelText = t("deleteDialog.confirmLabel", {
|
||||
ns: "common",
|
||||
entity: "workspace",
|
||||
})
|
||||
const textField = await screen.findByLabelText(labelText)
|
||||
const textField = within(dialog).getByLabelText(labelText)
|
||||
await user.type(textField, MockWorkspace.name)
|
||||
const confirmButton = await screen.findByRole("button", { name: "Delete" })
|
||||
const confirmButton = within(dialog).getByRole("button", {
|
||||
name: "Delete",
|
||||
hidden: false,
|
||||
})
|
||||
await user.click(confirmButton)
|
||||
expect(deleteWorkspaceMock).toBeCalled()
|
||||
// This test takes long to finish
|
||||
}, 20_000)
|
||||
})
|
||||
|
||||
it("requests a start job when the user presses Start", async () => {
|
||||
server.use(
|
||||
|
@ -157,7 +162,8 @@ describe("WorkspacePage", () => {
|
|||
|
||||
await renderWorkspacePage()
|
||||
|
||||
const cancelButton = await screen.findByRole("button", {
|
||||
const workspaceActions = screen.getByTestId("workspace-actions")
|
||||
const cancelButton = within(workspaceActions).getByRole("button", {
|
||||
name: "cancel action",
|
||||
})
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { screen, waitFor } from "@testing-library/react"
|
||||
import { screen } from "@testing-library/react"
|
||||
import { rest } from "msw"
|
||||
import * as CreateDayString from "util/createDayString"
|
||||
import {
|
||||
|
@ -37,25 +37,7 @@ describe("WorkspacesPage", () => {
|
|||
})
|
||||
|
||||
it("renders a filled workspaces page", async () => {
|
||||
// When
|
||||
const { container } = render(<WorkspacesPage />)
|
||||
|
||||
// Then
|
||||
const nextPage = await screen.findByRole("button", { name: "Next page" })
|
||||
expect(nextPage).toBeEnabled()
|
||||
await waitFor(
|
||||
async () => {
|
||||
const prevPage = await screen.findByRole("button", {
|
||||
name: "Previous page",
|
||||
})
|
||||
expect(prevPage).toBeDisabled()
|
||||
const pageButtons = container.querySelectorAll(
|
||||
`button[name="Page button"]`,
|
||||
)
|
||||
expect(pageButtons.length).toBe(2)
|
||||
},
|
||||
{ timeout: 2000 },
|
||||
)
|
||||
render(<WorkspacesPage />)
|
||||
await screen.findByText(`${MockWorkspace.name}1`)
|
||||
const templateDisplayNames = await screen.findAllByText(
|
||||
`${MockWorkspace.template_display_name}`,
|
||||
|
|
|
@ -307,4 +307,8 @@ export const handlers = [
|
|||
rest.get("/api/v2/appearance", (req, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockAppearance))
|
||||
}),
|
||||
|
||||
rest.get("/api/v2/deployment/stats", (_, res, ctx) => {
|
||||
return res(ctx.status(200), ctx.json(M.MockDeploymentStats))
|
||||
}),
|
||||
]
|
||||
|
|
|
@ -87,6 +87,6 @@ export function renderWithAuth(
|
|||
}
|
||||
|
||||
export const waitForLoaderToBeRemoved = (): Promise<void> =>
|
||||
waitForElementToBeRemoved(() => screen.getByRole("progressbar"))
|
||||
waitForElementToBeRemoved(() => screen.getByTestId("loader"))
|
||||
|
||||
export * from "./entities"
|
||||
|
|
1523
site/yarn.lock
1523
site/yarn.lock
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue