diff --git a/frontend/app/__tests__/auth.context.test.tsx b/frontend/app/__tests__/auth.context.test.tsx deleted file mode 100644 index 457404a..0000000 --- a/frontend/app/__tests__/auth.context.test.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import React, { useEffect } from 'react'; -import { Text } from 'react-native'; -import { render, screen, waitFor, act } from '@testing-library/react-native'; -import { AuthProvider, useAuth } from '../context/Auth'; - -const fetchMock = jest.fn(); -global.fetch = fetchMock as any; - -function ShowUser() { - const { user } = useAuth(); - return {user ? user.email : 'none'}; -} - -let exposed: ReturnType | null = null; -function Expose() { - exposed = useAuth(); - return null; -} - -beforeEach(() => { - fetchMock.mockReset(); - exposed = null; -}); - -test('AuthProvider provides default null user', () => { - function ShowUser() { - const { user } = useAuth(); - return {user ? 'yes' : 'no'}; - } - render(); - expect(screen.getByTestId('user').props.children).toBe('no'); -}); \ No newline at end of file diff --git a/frontend/app/__tests__/home.screen.test.tsx b/frontend/app/__tests__/home.screen.test.tsx index d79daf5..110e759 100644 --- a/frontend/app/__tests__/home.screen.test.tsx +++ b/frontend/app/__tests__/home.screen.test.tsx @@ -1,37 +1,62 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react'; +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import HomeScreen from "../screens/HomeScreen"; -// Mock pyodide to avoid network and heavy init -jest.mock('pyodide', () => ({ - loadPyodide: jest.fn().mockResolvedValue({ +// Mock pyodide (simulate loadPyodide and Python execution) +jest.mock("pyodide", () => ({ + loadPyodide: jest.fn(async () => ({ + runPythonAsync: jest.fn(async (code: string) => { + if (code.includes("error")) throw new Error("Simulated Python error"); + return "3\n"; // simulate print(3) + }), setStdout: jest.fn(), setStderr: jest.fn(), - runPythonAsync: jest.fn().mockResolvedValue(undefined), - }), + })), })); -// Mock clipboard used by Copy Output button -Object.assign(navigator, { clipboard: { writeText: jest.fn().mockResolvedValue(undefined) } }); +describe("HomeScreen", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); -// Mock react-navigation (only what HomeScreen touches indirectly) -jest.mock('@react-navigation/native', () => { - const actual = jest.requireActual('@react-navigation/native'); - return { ...actual, useNavigation: () => ({ navigate: jest.fn() }) }; -}); + it("renders title and buttons", async () => { + render(); + expect(await screen.findByText("Python Front")).toBeInTheDocument(); + expect(screen.getByText("Run")).toBeInTheDocument(); + expect(screen.getByText("Clear")).toBeInTheDocument(); + }); + + it("loads Pyodide successfully and shows Ready status", async () => { + render(); + await waitFor(() => { + expect(screen.getByText(/Pyodide: Ready/i)).toBeTruthy(); + }); + }); -// Mock useAuth to a simple anonymous state -jest.mock('../context/Auth', () => ({ useAuth: jest.fn(() => ({ user: null })) })); + it("runs Python code and shows output", async () => { + render(); + await waitFor(() => screen.getByText(/Ready/)); + fireEvent.click(screen.getByText("Run")); + await waitFor(() => + expect(screen.getByText(/3|no output/i)).toBeTruthy() + ); + }); -const HomeScreen = require('../screens/HomeScreen').default; + it("handles Python errors gracefully", async () => { + render(); + await waitFor(() => screen.getByText(/Ready/)); + const textarea = screen.getByRole("textbox"); + fireEvent.change(textarea, { target: { value: "error code" } }); + fireEvent.click(screen.getByText("Run")); + await waitFor(() => + expect(screen.getByText(/\[ERROR\]/i)).toBeTruthy() + ); + }); -test('renders title, code and console areas', async () => { - render(); - // Title link - expect(await screen.findByText('Python Front')).toBeTruthy(); - // Buttons (labels come from MUI Buttons) - expect(screen.getByRole('button', { name: /Run/i })).toBeTruthy(); - expect(screen.getByRole('button', { name: /Clear/i })).toBeTruthy(); - expect(screen.getByRole('button', { name: /Load Sample/i })).toBeTruthy(); - // Console label - expect(screen.getByText('Console')).toBeTruthy(); -}); \ No newline at end of file + it("clears console output", async () => { + render(); + await waitFor(() => screen.getByText(/Ready/)); + fireEvent.click(screen.getByText("Clear")); + expect(screen.getByText(/\(No output yet\./i)).toBeInTheDocument(); + }); +}); diff --git a/frontend/app/__tests__/settingsbar.test.tsx b/frontend/app/__tests__/settingsbar.test.tsx deleted file mode 100644 index 70909ae..0000000 --- a/frontend/app/__tests__/settingsbar.test.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { render, screen } from '@testing-library/react-native'; - -// Mock useAuth -jest.mock('../context/Auth', () => ({ useAuth: jest.fn() })); -const { useAuth } = require('../context/Auth') as { useAuth: jest.Mock }; - -// Mock useNavigation -const mockNavigate = jest.fn(); -jest.mock('@react-navigation/native', () => { - const actual = jest.requireActual('@react-navigation/native'); - return { ...actual, useNavigation: () => ({ navigate: mockNavigate }) }; -}); - -beforeEach(() => { useAuth.mockReset(); mockNavigate.mockReset(); }); - -test('SettingsBar renders without crashing', () => { - const SettingsBar = require('../components/SettingsBar').default; - useAuth.mockReturnValue({ user: null, token: null, authHeader: () => ({}) }); - render(); - // No strict text to assert (bar is mostly layout), just verify render - expect(true).toBe(true); -}); \ No newline at end of file diff --git a/frontend/app/package-lock.json b/frontend/app/package-lock.json index 02438bb..b5cb101 100644 --- a/frontend/app/package-lock.json +++ b/frontend/app/package-lock.json @@ -28,9 +28,12 @@ }, "devDependencies": { "@babel/core": "^7.25.2", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-native": "^5.4.3", + "@testing-library/react": "^16.3.0", "@testing-library/react-native": "^13.3.3", "@types/jest": "^30.0.0", + "@types/node": "^24.8.1", "@types/react": "^19.1.10", "babel-plugin-transform-inline-environment-variables": "^0.4.4", "jest": "~29.7.0", @@ -39,11 +42,7 @@ "react-native-reanimated": "^4.1.0", "react-test-renderer": "^19.1.0", "ts-node": "^10.9.2", - "typescript": "^5.9.2", - "@testing-library/react": "^16.0.0", - "react-dom": "^18.3.1", - "@testing-library/jest-dom": "^6.6.3", - "@types/jest": "29.5.14" + "typescript": "^5.9.2" } }, "node_modules/@0no-co/graphql.web": { @@ -60,6 +59,13 @@ } } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -4381,6 +4387,104 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/dom/node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/dom/node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/@testing-library/jest-native": { "version": "5.4.3", "resolved": "https://registry.npmjs.org/@testing-library/jest-native/-/jest-native-5.4.3.tgz", @@ -4433,6 +4537,34 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@testing-library/react-native": { "version": "13.3.3", "resolved": "https://registry.npmjs.org/@testing-library/react-native/-/react-native-13.3.3.tgz", @@ -4546,6 +4678,14 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4732,12 +4872,12 @@ "peer": true }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.8.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.8.1.tgz", + "integrity": "sha512-alv65KGRadQVfVcG69MuB4IzdYVpRwMG/mq8KWOaoOdyY617P5ivaDiMCGOFDWD2sAn5Q0mR3mRtUOgm99hL9Q==", "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.14.0" } }, "node_modules/@types/parse-json": { @@ -5296,6 +5436,16 @@ "node": ">=10" } }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -6373,6 +6523,13 @@ "hyphenate-style-name": "^1.0.3" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -6564,6 +6721,17 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -6619,6 +6787,13 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -10347,6 +10522,17 @@ "yallist": "^3.0.2" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -13789,9 +13975,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.14.0.tgz", + "integrity": "sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==", "license": "MIT" }, "node_modules/unicode-canonical-property-names-ecmascript": { diff --git a/frontend/app/package.json b/frontend/app/package.json index 91fe264..f722e98 100644 --- a/frontend/app/package.json +++ b/frontend/app/package.json @@ -31,9 +31,12 @@ }, "devDependencies": { "@babel/core": "^7.25.2", + "@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-native": "^5.4.3", + "@testing-library/react": "^16.3.0", "@testing-library/react-native": "^13.3.3", "@types/jest": "^30.0.0", + "@types/node": "^24.8.1", "@types/react": "^19.1.10", "babel-plugin-transform-inline-environment-variables": "^0.4.4", "jest": "~29.7.0", diff --git a/frontend/app/tsconfig.json b/frontend/app/tsconfig.json index 67d7fb0..5e6cea0 100644 --- a/frontend/app/tsconfig.json +++ b/frontend/app/tsconfig.json @@ -4,7 +4,7 @@ "strict": true, "moduleResolution": "bundler", "lib": ["esnext", "dom"], - "types": ["jest", "@testing-library/jest-dom"], + "types": ["jest", "@testing-library/jest-dom", "node"], "baseUrl": ".", "paths": { "pyodide": ["node_modules/pyodide/pyodide"]