Merge pull request #93 from mcataford/build/explore-vite-instead-of-parcel

build: explore vite instead of parcel
This commit is contained in:
Marc 2023-12-30 00:52:43 -05:00 committed by GitHub
commit b243e309d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 2253 additions and 3836 deletions

View file

@ -4,9 +4,6 @@
# This is expected to run before any step logic is executed. # This is expected to run before any step logic is executed.
# #
env:
NODE_VERSION: lts/iron
name: 'Setup Frontend Environment' name: 'Setup Frontend Environment'
inputs: inputs:
task-version: task-version:
@ -24,7 +21,7 @@ runs:
version: ${{ inputs.task-version }} version: ${{ inputs.task-version }}
- uses: actions/setup-node@v4 - uses: actions/setup-node@v4
with: with:
node-version: ${{ env.NODE_VERSION }} node-version: lts/iron
- uses: actions/cache@v3 - uses: actions/cache@v3
id: cache-restore id: cache-restore
with: with:

View file

@ -1,9 +0,0 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "jsdom",
setupFilesAfterEnv: ["./src/tests/testSetup.ts"],
transform: {
"^.+\\.(ts|tsx)$": "ts-jest",
},
}

View file

@ -11,32 +11,30 @@
"react-dom": "^18.2.0" "react-dom": "^18.2.0"
}, },
"scripts": { "scripts": {
"start": "parcel serve src/index.html --no-cache", "start": "vite ./src --config ./vite.config.js",
"build": "parcel build src/index.html", "build": "vite build ./src",
"lint": "biome check src *.js --verbose && biome format src *.js --verbose", "lint": "biome check src *.js --verbose && biome format src *.js --verbose",
"lint:fix": "biome check src ./*.js --apply --verbose && biome format src ./*.js --write --verbose", "lint:fix": "biome check src ./*.js --apply --verbose && biome format src ./*.js --write --verbose",
"test": "yarn jest", "test": "yarn vitest run src",
"typecheck": "yarn tsc --noEmit" "typecheck": "yarn tsc --noEmit"
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.4.1", "@biomejs/biome": "^1.4.1",
"@parcel/core": "^2.10.3",
"@parcel/types": "^2.10.3",
"@testing-library/dom": "^9.3.3", "@testing-library/dom": "^9.3.3",
"@testing-library/jest-dom": "^6.1.5", "@testing-library/jest-dom": "^6.1.5",
"@testing-library/react": "^14.1.2", "@testing-library/react": "^14.1.2",
"@testing-library/user-event": "^14.5.1", "@testing-library/user-event": "^14.5.1",
"@types/jest": "^29.5.3", "@types/mocha": "^10.0.6",
"@types/node": "^20.10.6", "@types/node": "^20.10.6",
"@types/react": "^18.2.18", "@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"@vitejs/plugin-basic-ssl": "^1.0.2",
"@vitejs/plugin-legacy": "^5.2.0",
"axios-mock-adapter": "^1.21.5", "axios-mock-adapter": "^1.21.5",
"buffer": "^5.5.0||^6.0.0", "jsdom": "^23.0.1",
"jest": "^29.7.0", "terser": "^5.26.0",
"jest-environment-jsdom": "^29.7.0", "typescript": "^5.3.0",
"parcel": "^2.10.3", "vite": "^5.0.10",
"process": "^0.11.10", "vitest": "^1.1.0"
"ts-jest": "^29.1.1",
"typescript": "^5.3.0"
} }
} }

View file

@ -1,3 +1,4 @@
import { vi, expect, describe, it } from "vitest"
import { act } from "@testing-library/react" import { act } from "@testing-library/react"
import { within } from "@testing-library/dom" import { within } from "@testing-library/dom"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
@ -19,9 +20,9 @@ describe("FileDetails", () => {
size: 1, size: 1,
id: "b61bf93d-a9db-473e-822e-a65003b1b7e3", id: "b61bf93d-a9db-473e-822e-a65003b1b7e3",
} }
test("Clicking the download button trigger a file download", async () => { it("Clicking the download button trigger a file download", async () => {
// FIXME: Validating file downloads is ... tricky. The current interaction with dynamically created DOM // FIXME: Validating file downloads is ... tricky. The current interaction with dynamically created DOM
// elements is not visible by jest. // elements is not visible by vi.
const expectedUrlPattern = new RegExp(`/files/${mockItem.id}/content/$`) const expectedUrlPattern = new RegExp(`/files/${mockItem.id}/content/$`)
@ -29,12 +30,10 @@ describe("FileDetails", () => {
axiosMock.onGet(expectedUrlPattern).reply(200, mockItem) axiosMock.onGet(expectedUrlPattern).reply(200, mockItem)
jest vi.spyOn(fileQueries, "useFileDetails").mockReturnValue({
.spyOn(fileQueries, "useFileDetails") data: mockItem,
.mockReturnValue({ data: mockItem, isLoading: false } as UseQueryResult< isLoading: false,
FileData, } as UseQueryResult<FileData, unknown>)
unknown
>)
const user = userEvent.setup() const user = userEvent.setup()
const { getByLabelText, debug, rerender } = render( const { getByLabelText, debug, rerender } = render(
@ -54,19 +53,17 @@ describe("FileDetails", () => {
expect(downloadRequest.url).toMatch(expectedUrlPattern) expect(downloadRequest.url).toMatch(expectedUrlPattern)
}) })
test("Clicking the delete button fires request to delete file", async () => { it("Clicking the delete button fires request to delete file", async () => {
const expectedUrlPattern = new RegExp(`/files/${mockItem.id}/$`) const expectedUrlPattern = new RegExp(`/files/${mockItem.id}/$`)
const axiosMock = getAxiosMockAdapter() const axiosMock = getAxiosMockAdapter()
axiosMock.onDelete(expectedUrlPattern).reply(200, mockItem) axiosMock.onDelete(expectedUrlPattern).reply(200, mockItem)
jest vi.spyOn(fileQueries, "useFileDetails").mockReturnValue({
.spyOn(fileQueries, "useFileDetails") data: mockItem,
.mockReturnValue({ data: mockItem, isLoading: false } as UseQueryResult< isLoading: false,
FileData, } as UseQueryResult<FileData, unknown>)
unknown
>)
const user = userEvent.setup() const user = userEvent.setup()
const { getByLabelText, debug, rerender } = render( const { getByLabelText, debug, rerender } = render(
@ -86,22 +83,20 @@ describe("FileDetails", () => {
expect(deleteRequest.url).toMatch(expectedUrlPattern) expect(deleteRequest.url).toMatch(expectedUrlPattern)
}) })
test("Clicking the delete button redirects to the file list after success", async () => { it("Clicking the delete button redirects to the file list after success", async () => {
const expectedUrlPattern = new RegExp(`/files/${mockItem.id}/$`) const expectedUrlPattern = new RegExp(`/files/${mockItem.id}/$`)
const axiosMock = getAxiosMockAdapter() const axiosMock = getAxiosMockAdapter()
axiosMock.onDelete(expectedUrlPattern).reply(200, mockItem) axiosMock.onDelete(expectedUrlPattern).reply(200, mockItem)
jest vi.spyOn(fileQueries, "useFileDetails").mockReturnValue({
.spyOn(fileQueries, "useFileDetails") data: mockItem,
.mockReturnValue({ data: mockItem, isLoading: false } as UseQueryResult< isLoading: false,
FileData, } as UseQueryResult<FileData, unknown>)
unknown
>)
const navigateMock = jest.fn().mockImplementation((a: string) => {}) const navigateMock = vi.fn().mockImplementation((a: string) => {})
jest.spyOn(locationContextUtils, "useLocationContext").mockReturnValue({ vi.spyOn(locationContextUtils, "useLocationContext").mockReturnValue({
location: { location: {
path: "", path: "",
label: null, label: null,

View file

@ -1,3 +1,4 @@
import { expect, describe, it, vi } from "vitest"
import { within } from "@testing-library/dom" import { within } from "@testing-library/dom"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
@ -28,14 +29,14 @@ describe("FileList", () => {
{ title: "Async Item 0", filename: "async.txt", size: 2, type: "upload" }, { title: "Async Item 0", filename: "async.txt", size: 2, type: "upload" },
] ]
test("Renders list items provided", () => { it("Renders list items provided", () => {
const { getAllByText } = render(<FileList data={mockItems} />) const { getAllByText } = render(<FileList data={mockItems} />)
const renderedItems = getAllByText(/Item/) const renderedItems = getAllByText(/Item/)
expect(renderedItems.length).toEqual(mockItems.length) expect(renderedItems.length).toEqual(mockItems.length)
}) })
test("Prepends items in flight as tracked by async task context", () => { it("Prepends items in flight as tracked by async task context", () => {
const { getAllByText, getByText } = render( const { getAllByText, getByText } = render(
<FileList data={[mockItems[0]]} />, <FileList data={[mockItems[0]]} />,
{ asyncTaskContext: mockAsyncTasks }, { asyncTaskContext: mockAsyncTasks },
@ -50,7 +51,7 @@ describe("FileList", () => {
}) })
describe("FileListItem", () => { describe("FileListItem", () => {
test("Renders the item title", () => { it("Renders the item title", () => {
const { getByLabelText, debug } = render( const { getByLabelText, debug } = render(
<FileList data={[mockItems[0]]} />, <FileList data={[mockItems[0]]} />,
) )
@ -58,7 +59,7 @@ describe("FileList", () => {
within(title).getByText(mockItems[0].title) within(title).getByText(mockItems[0].title)
}) })
test("Renders the item size", () => { it("Renders the item size", () => {
const { getByLabelText, debug } = render( const { getByLabelText, debug } = render(
<FileList data={[mockItems[0]]} />, <FileList data={[mockItems[0]]} />,
) )
@ -66,7 +67,7 @@ describe("FileList", () => {
within(title).getByText(`${mockItems[0].size} B`) within(title).getByText(`${mockItems[0].size} B`)
}) })
test.each(["download item", "delete item"])( it.each(["download item", "delete item"])(
"Renders secondary action buttons (%s)", "Renders secondary action buttons (%s)",
(action) => { (action) => {
const { getByLabelText, debug } = render( const { getByLabelText, debug } = render(
@ -76,7 +77,7 @@ describe("FileList", () => {
}, },
) )
test("Clicking the delete button fires request to delete file", async () => { it("Clicking the delete button fires request to delete file", async () => {
const expectedUrlPattern = new RegExp(`/files/${mockItems[0].id}/$`) const expectedUrlPattern = new RegExp(`/files/${mockItems[0].id}/$`)
const axiosMock = getAxiosMockAdapter() const axiosMock = getAxiosMockAdapter()
@ -101,9 +102,9 @@ describe("FileList", () => {
expect(deleteRequest.url).toMatch(expectedUrlPattern) expect(deleteRequest.url).toMatch(expectedUrlPattern)
}) })
test("Clicking the download button trigger a file download", async () => { it("Clicking the download button trigger a file download", async () => {
// FIXME: Validating file downloads is ... tricky. The current interaction with dynamically created DOM // FIXME: Validating file downloads is ... tricky. The current interaction with dynamically created DOM
// elements is not visible by jest. // elements is not visible by vi.
const expectedUrlPattern = new RegExp( const expectedUrlPattern = new RegExp(
`/files/${mockItems[0].id}/content/$`, `/files/${mockItems[0].id}/content/$`,
) )

View file

@ -1,3 +1,4 @@
import { vi, expect, describe, it, afterEach } from "vitest"
import { render, screen, waitFor } from "@testing-library/react" import { render, screen, waitFor } from "@testing-library/react"
import { QueryClientProvider, QueryClient } from "@tanstack/react-query" import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
import AxiosMockAdapter from "axios-mock-adapter" import AxiosMockAdapter from "axios-mock-adapter"
@ -21,11 +22,11 @@ function renderComponent() {
describe("FileListView", () => { describe("FileListView", () => {
afterEach(() => { afterEach(() => {
jest.resetAllMocks() vi.resetAllMocks()
}) })
it("renders no sidebar if no item is in the path", async () => { it("renders no sidebar if no item is in the path", async () => {
jest.spyOn(globalThis, "location", "get").mockReturnValue({ vi.spyOn(globalThis, "location", "get").mockReturnValue({
...globalThis.location, ...globalThis.location,
pathname: "/", pathname: "/",
}) })
@ -54,7 +55,7 @@ describe("FileListView", () => {
it("renders a sidebar if an item is selected", async () => { it("renders a sidebar if an item is selected", async () => {
const mockItemId = "b61bf93d-a9db-473e-822e-a65003b1b7e3" const mockItemId = "b61bf93d-a9db-473e-822e-a65003b1b7e3"
jest.spyOn(globalThis, "location", "get").mockReturnValue({ vi.spyOn(globalThis, "location", "get").mockReturnValue({
...globalThis.location, ...globalThis.location,
pathname: `/item/${mockItemId}/`, pathname: `/item/${mockItemId}/`,
}) })

View file

@ -1,3 +1,5 @@
import { expect, describe, it, vi } from "vitest"
import { render, screen, waitFor } from "@testing-library/react" import { render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
import { QueryClientProvider, QueryClient } from "@tanstack/react-query" import { QueryClientProvider, QueryClient } from "@tanstack/react-query"
@ -41,7 +43,7 @@ describe("LoginView", () => {
}) })
it("renders a registration link", async () => { it("renders a registration link", async () => {
const mock = jest.fn() const mock = vi.fn()
const { user } = renderComponent() const { user } = renderComponent()
expect(screen.getByText(/don\'t have an account yet?/i)).toBeInTheDocument() expect(screen.getByText(/don\'t have an account yet?/i)).toBeInTheDocument()
@ -113,8 +115,8 @@ describe("LoginView", () => {
}) })
it("redirects the user on success", async () => { it("redirects the user on success", async () => {
const mockNavigate = jest.fn() const mockNavigate = vi.fn()
const mockLocationHook = jest const mockLocationHook = vi
.spyOn(locationHook, "useLocationContext") .spyOn(locationHook, "useLocationContext")
.mockImplementation(() => ({ .mockImplementation(() => ({
location: { location: {

View file

@ -1,3 +1,4 @@
import { vi, it, describe, expect } from "vitest"
import { within } from "@testing-library/dom" import { within } from "@testing-library/dom"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
@ -11,12 +12,12 @@ import { type FileData } from "../../types/files"
describe("NavigationBar", () => { describe("NavigationBar", () => {
describe("Upload functionality", () => { describe("Upload functionality", () => {
test("Renders the upload button", () => { it("Renders the upload button", () => {
const { getByText } = render(<NavigationBar />) const { getByText } = render(<NavigationBar />)
getByText("Upload file") getByText("Upload file")
}) })
test("Clicking the upload button and selecting a file POSTs the file", async () => { it("Clicking the upload button and selecting a file POSTs the file", async () => {
const axiosMock = getAxiosMockAdapter() const axiosMock = getAxiosMockAdapter()
const expectedUrlPattern = new RegExp("/files/$") const expectedUrlPattern = new RegExp("/files/$")
axiosMock.onPost(expectedUrlPattern).reply(200, { axiosMock.onPost(expectedUrlPattern).reply(200, {

View file

@ -1,3 +1,4 @@
import { expect, it, vi, describe } from "vitest"
import { screen, render, waitFor } from "@testing-library/react" import { screen, render, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
import { QueryClientProvider, QueryClient } from "@tanstack/react-query" import { QueryClientProvider, QueryClient } from "@tanstack/react-query"

View file

@ -1,3 +1,4 @@
import { describe, it, expect } from "vitest"
import { validateEmail, validatePassword } from "./validation" import { validateEmail, validatePassword } from "./validation"
describe("Email address format validation", () => { describe("Email address format validation", () => {

View file

@ -1,3 +1,4 @@
import { describe, it, expect, vi } from "vitest"
import React from "react" import React from "react"
import { screen, render, waitFor } from "@testing-library/react" import { screen, render, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
@ -46,7 +47,7 @@ function renderComponent(props?: Partial<TextInputProps>) {
describe("TextInput", () => { describe("TextInput", () => {
it("runs the provided onChange on input", async () => { it("runs the provided onChange on input", async () => {
const mockOnChange = jest.fn() const mockOnChange = vi.fn()
const mockInput = "testinput" const mockInput = "testinput"
const { user } = renderComponent({ onChange: mockOnChange }) const { user } = renderComponent({ onChange: mockOnChange })

View file

@ -1,3 +1,5 @@
import React from "react"
import { describe, expect, it } from "vitest"
import { render, screen } from "@testing-library/react" import { render, screen } from "@testing-library/react"
import Route from "./Route" import Route from "./Route"

View file

@ -1,3 +1,4 @@
import { afterEach, describe, it, vi, expect } from "vitest"
import { render, screen } from "@testing-library/react" import { render, screen } from "@testing-library/react"
import { LocationContext } from "../contexts/LocationContext" import { LocationContext } from "../contexts/LocationContext"
@ -10,17 +11,17 @@ function renderComponent(component: React.ReactElement) {
describe("Router", () => { describe("Router", () => {
afterEach(() => { afterEach(() => {
jest.resetAllMocks() vi.resetAllMocks()
}) })
it("throws an error if no Route exists for the given location", () => { it("throws an error if no Route exists for the given location", () => {
jest.spyOn(globalThis, "location", "get").mockReturnValue({ vi.spyOn(globalThis, "location", "get").mockReturnValue({
...globalThis.location, ...globalThis.location,
pathname: "/doesnotexist", pathname: "/doesnotexist",
}) })
// Silence the error to avoid logspam in tests. // Silence the error to avoid logspam in tests.
jest.spyOn(console, "error").mockImplementation(() => {}) vi.spyOn(console, "error").mockImplementation(() => {})
expect(() => expect(() =>
renderComponent( renderComponent(
@ -34,7 +35,7 @@ describe("Router", () => {
}) })
it("renders the route matching the given location", () => { it("renders the route matching the given location", () => {
jest.spyOn(globalThis, "location", "get").mockReturnValue({ vi.spyOn(globalThis, "location", "get").mockReturnValue({
...globalThis.location, ...globalThis.location,
pathname: "/exists", pathname: "/exists",
}) })
@ -51,7 +52,7 @@ describe("Router", () => {
}) })
it("only renders the route that matches", () => { it("only renders the route that matches", () => {
jest.spyOn(globalThis, "location", "get").mockReturnValue({ vi.spyOn(globalThis, "location", "get").mockReturnValue({
...globalThis.location, ...globalThis.location,
pathname: "/matches", pathname: "/matches",
}) })

View file

@ -1,9 +1,10 @@
import { vi } from "vitest"
import "@testing-library/jest-dom" import "@testing-library/jest-dom"
// URL.createObjectURL does not exist in jest-jsdom. // URL.createObjectURL does not exist in jest-jsdom.
globalThis.URL.createObjectURL = jest globalThis.URL.createObjectURL = vi
.fn() .fn()
.mockImplementation(() => "http://localhost/downloadUrl") .mockImplementation(() => "http://localhost/downloadUrl")
// Clicking DOM objects is not implemented in jest-jsdom. // Clicking DOM objects is not implemented in jest-jsdom.
HTMLAnchorElement.prototype.click = jest.fn() HTMLAnchorElement.prototype.click = vi.fn()

View file

@ -11,6 +11,6 @@
"strict": true, "strict": true,
"noImplicitAny": true, "noImplicitAny": true,
"skipLibCheck": true, "skipLibCheck": true,
"types": ["jest", "node"] "types": ["node", "mocha"]
} }
} }

19
frontend/vite.config.js Normal file
View file

@ -0,0 +1,19 @@
import legacy from "@vitejs/plugin-legacy"
import basicSSL from "@vitejs/plugin-basic-ssl"
import { defineConfig } from "vite"
export default defineConfig({
plugins: [legacy(), basicSSL()],
server: {
port: 1234,
strictPort: true,
https: false,
},
test: {
environment: "jsdom",
setupFiles: ["./src/tests/testSetup.ts"],
testMatch: ["./src/**/*.test.tsx?"],
globals: true,
},
})

File diff suppressed because it is too large Load diff