chore(site): Make FE tests faster (#6543)

This commit is contained in:
Bruno Quaresma 2023-03-13 13:35:09 -03:00 committed by GitHub
parent 9b2abf0952
commit 813b54942f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 863 additions and 834 deletions

View File

@ -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

1
.gitignore vendored
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?$",

View File

@ -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: {

View File

@ -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": [

View File

@ -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()

View File

@ -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 && (

View File

@ -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}

View File

@ -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>
)

View File

@ -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>

View File

@ -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" },
],
})
})

View File

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

View File

@ -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}`,

View File

@ -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))
}),
]

View File

@ -87,6 +87,6 @@ export function renderWithAuth(
}
export const waitForLoaderToBeRemoved = (): Promise<void> =>
waitForElementToBeRemoved(() => screen.getByRole("progressbar"))
waitForElementToBeRemoved(() => screen.getByTestId("loader"))
export * from "./entities"

File diff suppressed because it is too large Load Diff