diff --git a/.github/workflows/set-pr-title.yml b/.github/workflows/set-pr-title.yml deleted file mode 100644 index 6e9b5b05a..000000000 --- a/.github/workflows/set-pr-title.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Set PR Title to First Commit Message - -on: - pull_request: - types: [opened, synchronize] - -jobs: - set-pr-title: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v2 - with: - # Fetches the entire commit history so that we can access the first commit - fetch-depth: 0 - - - name: Get first commit message - run: | - # Get the SHA of the first commit in the PR - FIRST_COMMIT_SHA=$(git log --reverse --format="%H" "${{ github.event.pull_request.base.sha }}..HEAD" | head -n 1) - # Get the message of the first commit in the PR - FIRST_COMMIT_MESSAGE=$(git log -n 1 --format=%B "$FIRST_COMMIT_SHA") - echo "FIRST_COMMIT_MESSAGE=$FIRST_COMMIT_MESSAGE" >> $GITHUB_ENV - - - name: Update PR title - run: gh pr edit "$PR_NUMBER" --title "$FIRST_COMMIT_MESSAGE" - env: - FIRST_COMMIT_MESSAGE: ${{ env.FIRST_COMMIT_MESSAGE }} - GITHUB_TOKEN: ${{ secrets.PIXELASS_PAT_BLIBLA }} - PR_NUMBER: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/test-client.yml b/.github/workflows/test-client.yml new file mode 100644 index 000000000..afb36357e --- /dev/null +++ b/.github/workflows/test-client.yml @@ -0,0 +1,26 @@ +name: Client Tests + +on: + push: + branches: [ alpha, beta, rc, main ] + pull_request: + types: [ opened, synchronize ] + branches: [ alpha, beta, rc, main ] + +jobs: + test-client: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Run Client tests + run: npm run test:client diff --git a/.github/workflows/test-e2e.yml b/.github/workflows/test-e2e.yml new file mode 100644 index 000000000..2581fd81e --- /dev/null +++ b/.github/workflows/test-e2e.yml @@ -0,0 +1,26 @@ +name: e2e Tests + +on: + push: + branches: [ alpha, beta, rc, main ] + pull_request: + types: [opened, synchronize] + branches: [ alpha, beta, rc, main ] + +jobs: + test-e2e: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Run e2e tests + run: npm run test:e2e diff --git a/.github/workflows/test-electron.yml b/.github/workflows/test-electron.yml new file mode 100644 index 000000000..d0a813889 --- /dev/null +++ b/.github/workflows/test-electron.yml @@ -0,0 +1,26 @@ +name: Electron Tests + +on: + push: + branches: [ alpha, beta, rc, main ] + pull_request: + types: [opened, synchronize] + branches: [ alpha, beta, rc, main ] + +jobs: + test-electron: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [18.x] + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + - name: Install dependencies + run: npm install + - name: Run Electron tests + run: npm run test:electron diff --git a/.gitignore b/.gitignore index 4c550eb18..7ea0fa76a 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ live-canvas-generate-image-output.png.tmp.png # test coverage +playwright-report diff --git a/jest.config.client.ts b/jest.config.client.ts new file mode 100644 index 000000000..3487af15e --- /dev/null +++ b/jest.config.client.ts @@ -0,0 +1,41 @@ +import { defaults } from "jest-config"; + +// Adjust the import path to your tsconfig.json file + +const jestConfig = { + ...defaults, + roots: ["/src/client"], + testMatch: ["**/?(*.)test.ts?(x)"], + transform: { + "^.+\\.(t|j)sx?$": [ + "@swc/jest", + { + jsc: { + transform: { + react: { + runtime: "automatic", + }, + }, + }, + }, + ], + }, + moduleNameMapper: { + "@/(.*)": "/src/client/$1", + "#/(.*)": "/src/shared/$1", + }, + collectCoverage: true, + coverageDirectory: "./coverage", + coverageProvider: "v8", + coverageReporters: ["lcov", "text", "json"], + coverageThreshold: { + global: { + lines: 80, + }, + }, + testEnvironment: "jsdom", + transformIgnorePatterns: ["/node_modules/"], + extensionsToTreatAsEsm: [".ts", ".tsx"], +}; + +export default jestConfig; diff --git a/jest.config.ts b/jest.config.electron.ts similarity index 72% rename from jest.config.ts rename to jest.config.electron.ts index f9107b7e7..ecbe0b06b 100644 --- a/jest.config.ts +++ b/jest.config.electron.ts @@ -4,10 +4,10 @@ import { defaults } from "jest-config"; const jestConfig = { ...defaults, - testMatch: ["**/?(*.)test.ts?(x)"], - testPathIgnorePatterns: [".e2e."], + roots: ["/src/electron"], + testMatch: ["**/?(*.)test.ts"], transform: { - "^.+\\.(t|j)sx?$": "@swc/jest", + "^.+\\.ts$": ["@swc/jest"], }, moduleNameMapper: { "@/(.*)": "/src/electron/future/$1", @@ -23,8 +23,8 @@ const jestConfig = { }, }, transformIgnorePatterns: ["/node_modules/"], - extensionsToTreatAsEsm: [".ts", ".tsx"], - setupFilesAfterEnv: ["/jest.setup.ts"], + extensionsToTreatAsEsm: [".ts"], + setupFilesAfterEnv: ["/jest.setup.electron.ts"], }; export default jestConfig; diff --git a/jest.setup.ts b/jest.setup.electron.ts similarity index 88% rename from jest.setup.ts rename to jest.setup.electron.ts index d787f1429..a338e4589 100644 --- a/jest.setup.ts +++ b/jest.setup.electron.ts @@ -1,11 +1,4 @@ jest.mock("electron", () => { - const mockBrowserWindow = jest.fn().mockImplementation(() => ({ - loadURL: jest.fn(), - on: jest.fn(), - once: jest.fn(), - close: jest.fn(), - })); - const mockWebContents = { send: jest.fn(), }; @@ -17,8 +10,13 @@ jest.mock("electron", () => { close: jest.fn(), webContents: mockWebContents, }; - - mockBrowserWindow.getFocusedWindow = jest.fn(() => mockFocusedWindow); + const mockBrowserWindow = jest.fn().mockImplementation(() => ({ + loadURL: jest.fn(), + on: jest.fn(), + once: jest.fn(), + close: jest.fn(), + getFocusedWindow: jest.fn(() => mockFocusedWindow), + })); return { BrowserWindow: mockBrowserWindow, diff --git a/package.json b/package.json index 0b74e92de..1f38e1554 100644 --- a/package.json +++ b/package.json @@ -21,9 +21,11 @@ "toc": "npx markdown-toc README.md -i", "tsc:noEmit": "tsc --noEmit", "caption:test": "ts-node-esm main/captions/misc.ts", - "test:unit": "jest --runInBand --config jest.config.ts --verbose", + "test:electron": "jest --runInBand --config jest.config.electron.ts --verbose", + "test:client": "jest --runInBand --config jest.config.client.ts --verbose", "pretest:e2e": "nextron build --no-pack", - "test:e2e": "npx playwright test" + "test:e2e:local": "npx playwright test", + "test:e2e": "xvfb-run --auto-servernum --server-args=\"-screen 0 1280x960x24\" -- npx playwright test" }, "dependencies": { "electron-context-menu": "3.6.1", diff --git a/playwright-report/index.html b/playwright-report/index.html deleted file mode 100644 index 76188316c..000000000 --- a/playwright-report/index.html +++ /dev/null @@ -1,69 +0,0 @@ - - - - - - - - - Playwright Test Report - - - - -
- - - - \ No newline at end of file diff --git a/playwright/index.test.ts b/playwright/index.test.ts index b8e9357e6..55adf008a 100644 --- a/playwright/index.test.ts +++ b/playwright/index.test.ts @@ -7,7 +7,9 @@ let page: Page; test.beforeAll(async () => { // Use package.main - electronApp = await electron.launch({ args: ["."] }); + electronApp = await electron.launch({ + args: ["."], + }); const isPackaged = await electronApp.evaluate(async ({ app }) => app.isPackaged); expect(isPackaged).toBe(false); diff --git a/src/client/ions/hooks/__tests__/color-scheme.test.tsx b/src/client/ions/hooks/__tests__/color-scheme.test.tsx new file mode 100644 index 000000000..bc7a6b903 --- /dev/null +++ b/src/client/ions/hooks/__tests__/color-scheme.test.tsx @@ -0,0 +1,38 @@ +import type { Mode } from "@mui/system/cssVars/useCurrentColorScheme"; +import { renderHook, act } from "@testing-library/react"; + +import { useSsrColorScheme } from "../color-scheme"; + +function createMockUseColorScheme(initialMode = "system") { + let internalMode = initialMode; + + return jest.fn().mockImplementation(() => ({ + mode: internalMode, + setMode(newMode: Mode) { + internalMode = newMode; + }, + })); +} + +jest.mock("@mui/joy/styles", () => ({ + useColorScheme: createMockUseColorScheme(), +})); + +describe("useSsrColorScheme", () => { + it("should set initial mode to 'system'", () => { + const { result } = renderHook(() => useSsrColorScheme()); + expect(result.current.mode).toBe("system"); + }); + + it("should change mode when setMode is called", () => { + const { result, rerender } = renderHook(() => useSsrColorScheme()); // Get the rerender function + + act(() => { + result.current.setMode("light"); + }); + + rerender(); + + expect(result.current.mode).toBe("light"); + }); +}); diff --git a/src/client/ions/hooks/__tests__/columns.test.tsx b/src/client/ions/hooks/__tests__/columns.test.tsx new file mode 100644 index 000000000..2ae78ed8b --- /dev/null +++ b/src/client/ions/hooks/__tests__/columns.test.tsx @@ -0,0 +1,30 @@ +// UseColumns.test.tsx +import { extendTheme, ThemeProvider } from "@mui/joy/styles"; +import { render } from "@testing-library/react"; +import React from "react"; +import "@testing-library/jest-dom"; + +import { useColumns } from "../columns"; // Adjust the import path as necessary + +// Mock component to utilize the useColumns hook +function MockComponent({ xs, sm, md, lg }: { xs: number; sm: number; md: number; lg: number }) { + const columns = useColumns({ xs, sm, md, lg }); + return
{columns}
; +} + +describe("useColumns", () => { + const theme = extendTheme(); // Use your custom theme if you have one + + function renderWithTheme(properties: { xs: number; sm: number; md: number; lg: number }) { + return render( + + + + ); + } + + it("should return xs value for initial render", () => { + const { getByTestId } = renderWithTheme({ xs: 2, sm: 4, md: 6, lg: 8 }); + expect(getByTestId("column-count")).toHaveTextContent("2"); + }); +}); diff --git a/src/client/ions/hooks/__tests__/keyboard-controlled-images-navigation.test.tsx b/src/client/ions/hooks/__tests__/keyboard-controlled-images-navigation.test.tsx new file mode 100644 index 000000000..9127de13b --- /dev/null +++ b/src/client/ions/hooks/__tests__/keyboard-controlled-images-navigation.test.tsx @@ -0,0 +1,94 @@ +import { fireEvent, waitFor } from "@testing-library/react"; +import { renderHook, act } from "@testing-library/react"; +import { Provider } from "jotai"; + +import { useKeyboardControlledImagesNavigation } from "../keyboard-controlled-images-navigation"; + +describe("useKeyboardControlledImagesNavigation", () => { + it("should handle keyboard navigation correctly", async () => { + const onBeforeChangeMock = jest.fn(); + // Render the hook that we are testing + renderHook( + () => useKeyboardControlledImagesNavigation({ onBeforeChange: onBeforeChangeMock }), + { + wrapper: Provider, + } + ); + + // Set initial state for images and selectedImage + act(() => { + fireEvent.keyDown(window, { key: "ArrowRight", altKey: true }); + }); + + // Assertions + await waitFor(() => { + expect(onBeforeChangeMock).toHaveBeenCalledTimes(1); + }); + act(() => { + fireEvent.keyDown(window, { key: "ArrowLeft", altKey: true }); + }); + + // Assertions + await waitFor(() => { + expect(onBeforeChangeMock).toHaveBeenCalledTimes(2); + }); + act(() => { + fireEvent.keyDown(window, { key: "ArrowUp", altKey: true }); + }); + + // Assertions + await waitFor(() => { + expect(onBeforeChangeMock).toHaveBeenCalledTimes(3); + }); + act(() => { + fireEvent.keyDown(window, { key: "ArrowDown", altKey: true }); + }); + + // Assertions + await waitFor(() => { + expect(onBeforeChangeMock).toHaveBeenCalledTimes(4); + }); + }); + it("should not respond with alt key", async () => { + const onBeforeChangeMock = jest.fn(); + + // Render the hook that we are testing + renderHook( + () => useKeyboardControlledImagesNavigation({ onBeforeChange: onBeforeChangeMock }), + { + wrapper: Provider, + } + ); + + // Set initial state for images and selectedImage + act(() => { + fireEvent.keyDown(window, { key: "ArrowRight" }); + }); + + // Assertions + await waitFor(() => { + expect(onBeforeChangeMock).not.toHaveBeenCalled(); + }); + }); + it("should not respond to other keys", async () => { + const onBeforeChangeMock = jest.fn(); + + // Render the hook that we are testing + renderHook( + () => useKeyboardControlledImagesNavigation({ onBeforeChange: onBeforeChangeMock }), + { + wrapper: Provider, + } + ); + + // Set initial state for images and selectedImage + act(() => { + fireEvent.keyDown(window, { key: "Enter", altKey: true }); + }); + + // Assertions + await waitFor(() => { + expect(onBeforeChangeMock).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/client/ions/hooks/__tests__/scroll-position.test.tsx b/src/client/ions/hooks/__tests__/scroll-position.test.tsx new file mode 100644 index 000000000..e3914b1bc --- /dev/null +++ b/src/client/ions/hooks/__tests__/scroll-position.test.tsx @@ -0,0 +1,77 @@ +import { act, fireEvent, render } from "@testing-library/react"; +import React, { useRef } from "react"; + +import { useScrollPosition } from "../scroll-position"; // Adjust your import path +import "@testing-library/jest-dom"; + +class ResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} + +global.ResizeObserver = ResizeObserver; +// Test component that uses your hook +function TestComponent() { + const reference = useRef(null); + const scroll = useScrollPosition(reference); + + return ( +
+
+
Scroll me
+
+

{scroll.scrollable ? "true" : "false"}

+

{scroll.start ? "true" : "false"}

+

{scroll.end ? "true" : "false"}

+
+ ); +} + +describe("useScrollPosition", () => { + beforeEach(() => { + // Mock properties before each test + Object.defineProperty(HTMLElement.prototype, "scrollWidth", { + configurable: true, + value: 300, + }); + Object.defineProperty(HTMLElement.prototype, "clientWidth", { + configurable: true, + value: 100, + }); + }); + + it("should correctly detect scroll positions", async () => { + const { getByTestId } = render(); + + expect(getByTestId("scrollable")).toHaveTextContent("true"); + expect(getByTestId("start")).toHaveTextContent("true"); + expect(getByTestId("end")).toHaveTextContent("false"); + }); + + it("should update scroll position on scroll event", async () => { + const { getByTestId } = render(); + const scrollableDiv = getByTestId("scrollable-div"); // Add this data-testid to your scrollable div + + // Check initial state + expect(getByTestId("scrollable")).toHaveTextContent("true"); + expect(getByTestId("start")).toHaveTextContent("true"); + expect(getByTestId("end")).toHaveTextContent("false"); + + // Simulate scrolling + act(() => { + // Manually set the scroll position + scrollableDiv.scrollLeft = 150; // Halfway scrolled + // Dispatch the scroll event on the scrollable div + fireEvent.scroll(scrollableDiv); + }); + + // Check the updated state after scrolling + expect(getByTestId("start")).toHaveTextContent("false"); + expect(getByTestId("end")).toHaveTextContent("false"); // Still not at the end + }); +}); diff --git a/src/client/ions/hooks/keyboard-controlled-images-navigation.tsx b/src/client/ions/hooks/keyboard-controlled-images-navigation.ts similarity index 60% rename from src/client/ions/hooks/keyboard-controlled-images-navigation.tsx rename to src/client/ions/hooks/keyboard-controlled-images-navigation.ts index d6b9bde62..abfe97232 100644 --- a/src/client/ions/hooks/keyboard-controlled-images-navigation.tsx +++ b/src/client/ions/hooks/keyboard-controlled-images-navigation.ts @@ -1,4 +1,4 @@ -import { useAtom } from "jotai/index"; +import { useAtom } from "jotai"; import { useCallback, useEffect } from "react"; import { imagesAtom, selectedImageAtom } from "@/ions/atoms"; @@ -10,40 +10,32 @@ export function useKeyboardControlledImagesNavigation({ onBeforeChange(): Promise | void; }) { const [images] = useAtom(imagesAtom); - const [selectedImage, setSelectedImage] = useAtom(selectedImageAtom); + const [, setSelectedImage] = useAtom(selectedImageAtom); const columnCount = useColumns({ xs: 2, sm: 3, md: 4, lg: 6 }); const { length: imagesLength } = images; const goRowUp = useCallback(() => { - if (selectedImage > columnCount - 1) { - setSelectedImage(selectedImage - columnCount); - } else { - setSelectedImage(imagesLength - 1); - } - }, [selectedImage, columnCount, setSelectedImage, imagesLength]); + setSelectedImage(previousState => + previousState > columnCount - 1 ? previousState - columnCount : imagesLength - 1 + ); + }, [columnCount, setSelectedImage, imagesLength]); const goRowDown = useCallback(() => { - if (selectedImage < imagesLength - columnCount) { - setSelectedImage(selectedImage + columnCount); - } else { - setSelectedImage(0); - } - }, [selectedImage, imagesLength, columnCount, setSelectedImage]); + setSelectedImage(previousState => + previousState < imagesLength - columnCount ? previousState + columnCount : 0 + ); + }, [imagesLength, columnCount, setSelectedImage]); const goToPrevious = useCallback(() => { - if (selectedImage > 0) { - setSelectedImage(selectedImage - 1); - } else { - setSelectedImage(imagesLength - 1); - } - }, [selectedImage, setSelectedImage, imagesLength]); + setSelectedImage(previousState => + previousState > 0 ? previousState - 1 : imagesLength - 1 + ); + }, [setSelectedImage, imagesLength]); const goToNext = useCallback(() => { - if (selectedImage < imagesLength - 1) { - setSelectedImage(selectedImage + 1); - } else { - setSelectedImage(0); - } - }, [selectedImage, imagesLength, setSelectedImage]); + setSelectedImage(previousState => + previousState < imagesLength - 1 ? previousState + 1 : 0 + ); + }, [imagesLength, setSelectedImage]); useEffect(() => { async function handleKeyDown(event: KeyboardEvent) {