diff --git a/.env.local.EXAMPLE b/.env.local.EXAMPLE new file mode 100644 index 0000000..a80e9dc --- /dev/null +++ b/.env.local.EXAMPLE @@ -0,0 +1,3 @@ + +# Local development database URL +DATABASE_URL=file:./dev.db diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..0038ea0 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,12 @@ +node_modules/ +.next/ +funey.db +schema.dump +schema.sqlite.sql +scripts/ +public/ +attached_assets/ +__tests__/ +**/.eslintrc.js +jest.config.js +jest.setup.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..25ddac8 --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,93 @@ +module.exports = { + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: __dirname, + ecmaVersion: 2020, + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + plugins: ['@typescript-eslint', 'react', 'import'], + extends: [ + 'next/core-web-vitals', + 'plugin:react/recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + ], + env: { + browser: true, + es6: true, + node: true, + }, + settings: { + 'import/resolver': { + node: { + extensions: ['.js', '.jsx', '.ts', '.tsx'], + moduleDirectory: ['node_modules', 'src/'], + }, + }, + }, + rules: { + semi: ['error', 'never'], + indent: ['error', 2], + 'no-undef': 'off', + 'react/jsx-filename-extension': [2, { extensions: ['.js', '.jsx', '.ts', '.tsx'] }], + 'react/function-component-definition': [ + 2, + { namedComponents: 'arrow-function', unnamedComponents: 'arrow-function' }, + ], + 'react/react-in-jsx-scope': 'off', + 'import/extensions': [ + 'error', + 'always', + { js: 'never', jsx: 'never', ts: 'never', tsx: 'never' }, + ], + 'react/jsx-props-no-spreading': 'off', + 'react/jsx-one-expression-per-line': 'off', + 'import/no-cycle': 'off', + 'import/named': 'off', + 'import/no-unresolved': 'off', + 'import/no-named-as-default': 'off', + 'import/no-named-as-default-member': 'off', + }, + overrides: [ + { + files: ['**/*.ts', '**/*.tsx'], + rules: { + 'react/prop-types': 'off', + 'import/order': 'off', + // TypeScript files: relax stylistic rules; Prettier handles formatting + semi: 'off', + '@typescript-eslint/semi': 'off', + quotes: 'off', + '@typescript-eslint/quotes': 'off', + indent: 'off', + '@typescript-eslint/indent': 'off', + 'comma-dangle': 'off', + '@typescript-eslint/comma-dangle': 'off', + 'object-curly-newline': 'off', + '@typescript-eslint/object-curly-spacing': 'off', + 'object-curly-spacing': 'off', + 'function-paren-newline': 'off', + '@typescript-eslint/return-await': 'off', + 'no-return-await': 'off', + 'eol-last': 'off', + camelcase: 'off', + 'no-param-reassign': 'off', + 'no-console': 'off', + '@typescript-eslint/no-explicit-any': 'off', + 'no-unused-vars': 'off', + '@typescript-eslint/no-unused-vars': 'off', + 'no-multiple-empty-lines': 'off', + 'max-len': 'off', + 'react/jsx-wrap-multilines': 'off', + 'react/jsx-indent': 'off', + 'react/jsx-closing-tag-location': 'off', + 'react/jsx-indent-props': 'off', + 'no-useless-escape': 'off', + }, + }, + ], +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..99054db --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,47 @@ +name: Node.js CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build_and_test: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [18.x, 20.x] # Test against relevant Node.js versions + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'yarn' # Cache yarn dependencies + + - name: Install dependencies + run: yarn install --frozen-lockfile + + - name: Run linters + run: yarn lint # Ensure code style and quality + + # Prisma schemas should be generated during the build or test setup if needed + # The setup-test-db.js script checks for generated schemas + # Ensure build:prisma-schemas is run if schemas aren't checked in + # - name: Build Prisma Schemas if necessary + # run: yarn build:prisma-schemas + + - name: Build project + run: yarn build + + - name: Run tests + run: yarn test + env: + # DATABASE_URL is set by the jest globalSetup script (setup-test-db.js) + # Add any other necessary env vars here if required by tests + CI: true # Often useful to indicate running in a CI environment diff --git a/.gitignore b/.gitignore index 1437c53..707834b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ + +.db/ +logfile +.env.local +funey.db + # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies @@ -32,3 +38,4 @@ yarn-error.log* # vercel .vercel +dev.db diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..7be555c --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1,28 @@ +#!/usr/bin/env sh + +# Load NVM to ensure correct Node version and PATH +export NVM_DIR="$HOME/.nvm" +if [ -s "$NVM_DIR/nvm.sh" ]; then + . "$NVM_DIR/nvm.sh" # Source NVM script if it exists + # Optionally, add nvm use command here if you need a specific version + # nvm use +elif [ -x "$(command -v brew)" ] && [ -s "$(brew --prefix nvm)/nvm.sh" ]; then + . "$(brew --prefix nvm)/nvm.sh" # Source NVM script if installed via Homebrew +fi + +# Check if nvm command is available after sourcing +if command -v nvm &> /dev/null; then + # Attempt to set the correct node version path if NVM is active + CURRENT_NODE_VERSION=$(nvm current) + NVM_NODE_PATH=$(nvm_find_nvmrc | xargs nvm_version_path 2>/dev/null || nvm_version_path "$CURRENT_NODE_VERSION" 2>/dev/null) + + if [ -n "$NVM_NODE_PATH" ]; then + export PATH="$NVM_NODE_PATH/bin:$PATH" + fi +else + echo "Warning: NVM script not found or NVM command not available. Falling back to system PATH." + # Fallback: Add local node_modules/.bin just in case NVM isn't used or fails + export PATH="$(cd "$(dirname "$0")/.." && pwd)/node_modules/.bin:$PATH" +fi + +npx lint-staged diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..61e0a18 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,12 @@ +node_modules/ +.next/ +funey.db +schema.dump +schema.sqlite.sql +scripts/ +public/ +attached_assets/ +__tests__/ +**/*.min.js +*.lock +yarn.lock \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..567c5d2 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "semi": false, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "endOfLine": "lf" +} \ No newline at end of file diff --git a/.replit b/.replit new file mode 100644 index 0000000..1dfac70 --- /dev/null +++ b/.replit @@ -0,0 +1,20 @@ +modules = ["nodejs-20"] +[workflows] +runButton = "Development" + +[[workflows.workflow]] +name = "Development" +author = 41832294 +mode = "sequential" + +[[workflows.workflow.tasks]] +task = "shell.exec" +args = "yarn dev" + +[[ports]] +localPort = 3000 +externalPort = 80 + +[nix] +packages = ["postgresql", "sqlite"] +channel = "stable-24_05" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2628f21..bcd2a6f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,72 +4,80 @@ Thank you for your interest in contributing to Funey! ## Getting Started -1. Fork the repository and clone your fork: +1. Fork and clone the repository: ```bash - git clone git@github.com:/funey.git + git clone https://github.com//funey.git cd funey ``` 2. Install dependencies: ```bash yarn install ``` -3. Set up the database: - - Create a new Postgres database. - - Load the schema: - ```bash - psql < schema.dump - ``` -4. Create a `.env.local` file in the project root and define your database connection: +3. Configure environment: + 1. Copy the example env file and edit: + ```bash + cp .env.local.EXAMPLE .env.local + ``` + 2. In `.env.local`, set `DATABASE_URL`: + - For local SQLite development: + ```ini + DATABASE_URL=file:./dev.db + ``` + - For PostgreSQL development: + ```ini + DATABASE_URL=postgresql://user:pass@localhost:5432/funey + ``` +4. Initialize the database: ```bash - PGHOST=localhost - PGPORT=5432 - PGPASS= - DB= + yarn build:prisma-schemas + yarn db:clean ``` -5. Start the development server: +5. Start development server: ```bash yarn dev ``` -Open http://localhost:3000 in your browser to verify everything is working. +## Code Style and Quality -## Code Style +- Indentation: 4 spaces +- Strings: double quotes +- Semicolons: follow existing style +- Use functional React components +- Organize pages under `/pages` +- Keep shared components/utilities in `/src` +- Update `schema.dump` after DB changes -- Indentation: 2 spaces -- Strings: single quotes in JS; double quotes in JSX attributes -- No semicolons at the end of statements -- Functional React components and Next.js conventions -- Organize pages under `/pages`; shared components or utilities under `/src` -- After any DB schema change, regenerate `schema.dump` +**Automated Checks:** + +This project uses `eslint` and `prettier` for code linting and formatting. These checks are automatically run on staged files before each commit using `husky` and `lint-staged`. + +- Ensure your code passes `yarn lint` before pushing. +- Commits may be blocked if linting or formatting errors are found. ## Commit Messages Follow [Conventional Commits](https://www.conventionalcommits.org/): -- `feat(scope): description` – a new feature -- `fix(scope): description` – a bug fix -- `docs(scope): description` – documentation only changes -- `style(scope): description` – formatting, missing semicolons, etc -- `refactor(scope): description` – code change that neither fixes a bug nor adds a feature -- `test(scope): description` – adding or updating tests -- `chore(scope): description` – changes to build process or auxiliary tools - -## Pull Requests -- Base your PR against the `main` branch -- Include a clear description of your changes -- Link to any relevant issues -- Add screenshots or GIFs for UI changes +- `feat:` new feature +- `fix:` bug fix +- `docs:` documentation changes +- `style:` formatting changes +- `refactor:` code restructuring +- `test:` adding/updating tests +- `chore:` build process changes -## Issues +## Pull Requests -Please use GitHub Issues to report bugs or suggest features. +- Base against `main` branch +- Include clear description +- Link relevant issues +- Add screenshots for UI changes ## Testing -- There are currently no automated tests. -- Manually verify your changes by running `yarn dev` and checking the app. - -## Linting - -- You can run `npx next lint` to check for lint errors. - (Add an ESLint config if needed.) \ No newline at end of file +- Manually verify changes with `yarn dev` +- Run unit tests locally: + ```bash + yarn test + ``` +- **Automated CI:** All pull requests and pushes to `main` automatically trigger a GitHub Actions workflow. This workflow runs linters (`yarn lint`), builds the project (`yarn build`), and executes the test suite (`yarn test`) across multiple Node.js versions. Ensure these checks pass before merging. diff --git a/README.md b/README.md index c339ac6..d8e0e1c 100644 --- a/README.md +++ b/README.md @@ -1,56 +1,80 @@ -This is a web app for managing an online "ledger" for your little one. Making money fun (funey? My 5 year old suggested it). +# Funey - Making Money Fun for Kids -Supports: +A web app for managing a virtual "ledger" for your children. Making money fun (funey? My 5 year old suggested it). -* Parent login on a per account basis (so that you can add / subtract money from the ledger) -* View only link for children (so they can see how much money they have) -* Automatic monthly interest calculation (So they can watch their funey grow) -* Automatic weekly allowance calculation -* Mobile device friendly display (Can be added as a home icon so they can view their totals from ipod / iphone / chromebook etc) +## Features -You can create your own account and play around at: +- Parent login for managing accounts (add/subtract money) +- View-only link for children +- Automatic monthly interest calculation +- Automatic weekly allowance +- Mobile-friendly display (can be added as a home icon) -https://funey.badpirate.net - -Or host it yourself. - -## Warning +## Development -This is for tracking a virtual balance, not the storage of actual money and while I've kept funey.badpirate.net -up for a number of years (and plan to keep hosting until my kids are in college), I -make no guaruntee that something won't happen to hosting or storage, use at your own risk. +### Database Setup -## Privacy +Funey uses Prisma for database access, with support for both SQLite (local/dev) and PostgreSQL (production or advanced testing). -You can review the code yourself, but I've intentially left no place to store email addresses or account names, -though the transactions themselves are not stored encrypted. If you host yourself I have no access to your data -or transactions. +1. Copy the example environment file: + ```bash + cp .env.local.EXAMPLE .env.local + ``` +2. In `.env.local`, set `DATABASE_URL`: + - For local development (SQLite): + ```ini + DATABASE_URL=file:./dev.db + ``` + - For PostgreSQL (e.g. CI or prod): + ```ini + DATABASE_URL=postgresql://user:password@localhost:5432/funey + ``` -If you choose to use funey.badpirate.net, I will not share the transaction details / descriptions / values with -third parties, or try to associate them with accounts or IP addresses. This is mostly a tool for myself that -I believe might have value for other parents and so leave open in hopes that it can be a benefit. There will be -no commercial or private use of your data. +### Initializing the Database -## Development +Funey provides helper scripts in `package.json`: -### Setup DB +- `yarn build:prisma-schemas` + Regenerates the Prisma schema variants (`prisma/generated/sqlite.prisma` and `prisma/generated/postgresql.prisma`). +- `yarn db:clean` + Drops (if SQLite) and recreates the database specified by `DATABASE_URL`, and generates the Prisma Client against that schema. For SQLite, this resets `dev.db` in the project root. -You'll need to setup a Postgres SQL database to run locally: +To start from a clean slate locally: -1. Setup a new database -2. Upload the default schema into your DB, `psql dbname < schema.dump` -3. Set `PGHOST`, `PGPORT`, `PGPASS` and `DB` values in `.env.local` (and your production env) +```bash +yarn install +yarn build:prisma-schemas +yarn db:clean +yarn dev +``` -### Getting Started +Open [http://localhost:3000](http://localhost:3000) to view the app. -First, run the development server: +### Running Locally ```bash +yarn install yarn dev ``` -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. +Open [http://localhost:3000](http://localhost:3000) to view the app. + +## Code Quality + +This project uses ESLint and Prettier for code linting and formatting. These checks are enforced automatically before each commit using Husky and lint-staged. + +Additionally, a GitHub Actions workflow runs on every push and pull request to the `main` branch, executing linters, build steps, and automated tests to ensure code quality and stability. + +## Warning + +This is for tracking virtual balances, not actual money storage. While stable, use at your own risk. + +## Privacy + +No email addresses or account names are stored. Transactions are not encrypted but are private to your instance. ## License -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. \ No newline at end of file +MIT License - See LICENSE file for details. + +Workspace Check: This line was added to verify workspace sharing. diff --git a/__tests__/CreateAccountForm.test.tsx b/__tests__/CreateAccountForm.test.tsx new file mode 100644 index 0000000..70e52e6 --- /dev/null +++ b/__tests__/CreateAccountForm.test.tsx @@ -0,0 +1,225 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import CreateAccountForm from '@/components/CreateAccountForm'; +import fetchMock from 'jest-fetch-mock'; // Import fetchMock + +describe('CreateAccountForm', () => { + beforeEach(() => { + // Reset fetch mock before each test + fetchMock.resetMocks(); + }); + + it('renders the form correctly', () => { + render(); + expect(screen.getByLabelText(/^Username$/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/^Password$/i)).toBeInTheDocument(); + expect(screen.getByLabelText(/Repeat Password/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /Create Account/i })).toBeInTheDocument(); + }); + + it('allows user to type into username and password fields', async () => { + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + + await user.type(usernameInput, 'testuser'); + await user.type(passwordInput, 'password123'); + await user.type(repeatPasswordInput, 'password123'); + + expect(usernameInput).toHaveValue('testuser'); + expect(passwordInput).toHaveValue('password123'); + expect(repeatPasswordInput).toHaveValue('password123'); + }); + + it('submits the form, calls the API, and shows success message on successful creation', async () => { + // Mock a successful API response + fetchMock.mockResponseOnce(JSON.stringify({ message: 'Account created successfully!' }), { status: 201 }); + + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + const testUsername = 'new_unique_user'; + const testPassword = 'newpassword123'; + + await user.type(usernameInput, testUsername); + await user.type(passwordInput, testPassword); + await user.type(repeatPasswordInput, testPassword); + await user.click(submitButton); + + // Check that fetch was called correctly + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith('/api/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: testUsername, password: testPassword }), + }); + }); + + // Check for the success message displayed by the component + expect(await screen.findByText(/Account created successfully!/i)).toBeInTheDocument(); + }); + + it('shows an error message if username already exists', async () => { + // Mock an API response indicating the user already exists + fetchMock.mockResponseOnce(JSON.stringify({ error: 'Username already exists' }), { status: 409 }); + + const user = userEvent.setup(); + const existingUsername = 'existinguser'; // No need to pre-create via fetch + + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + await user.type(usernameInput, existingUsername); + await user.type(passwordInput, 'password'); + await user.type(repeatPasswordInput, 'password'); + await user.click(submitButton); + + // Check that fetch was called correctly + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledTimes(1); + expect(fetchMock).toHaveBeenCalledWith('/api/create', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username: existingUsername, password: 'password' }), + }); + }); + + // Check for the error message displayed by the component + expect(await screen.findByText(/Username already exists/i)).toBeInTheDocument(); + }); + + it('shows a specific error message on API failures when provided', async () => { + // Mock a generic server error with a specific message + const specificError = 'Internal Server Error'; + fetchMock.mockResponseOnce(JSON.stringify({ error: specificError }), { status: 500 }); + + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + await user.type(usernameInput, 'someuser'); + await user.type(passwordInput, 'somepassword'); + await user.type(repeatPasswordInput, 'somepassword'); + await user.click(submitButton); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + // Check for the specific error message from the API response + expect(await screen.findByText(specificError)).toBeInTheDocument(); + }); + + it('shows a fallback error message on API failures without specific message', async () => { + // Mock a server error without a specific error message in the body + fetchMock.mockResponseOnce(JSON.stringify({}), { status: 500 }); + + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + await user.type(usernameInput, 'anotheruser'); + await user.type(passwordInput, 'anotherpassword'); + await user.type(repeatPasswordInput, 'anotherpassword'); + await user.click(submitButton); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + // Check for the component's fallback error message + expect(await screen.findByText(/Failed to create account/i)).toBeInTheDocument(); + }); + + it('shows a generic error message on network failure', async () => { + // Mock a network error + fetchMock.mockRejectOnce(new Error('Network failure')); + + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + await user.type(usernameInput, 'networkuser'); + await user.type(passwordInput, 'networkpassword'); + await user.type(repeatPasswordInput, 'networkpassword'); + await user.click(submitButton); + + await waitFor(() => { + expect(fetchMock).toHaveBeenCalledTimes(1); + }); + + // Check for the generic catch block error message + expect(await screen.findByText(/An unexpected error occurred. Please try again./i)).toBeInTheDocument(); + }); + + it('shows an error message if username is too short', async () => { + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + await user.type(usernameInput, 'usr'); // Too short + await user.type(passwordInput, 'password123'); + await user.type(repeatPasswordInput, 'password123'); + await user.click(submitButton); + + expect(await screen.findByText(/Username must be at least 4 characters long/i)).toBeInTheDocument(); + expect(fetchMock).not.toHaveBeenCalled(); // Verify fetch was not called + }); + + it('shows an error message if password is too short', async () => { + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + await user.type(usernameInput, 'testuser'); + await user.type(passwordInput, 'pass'); // Too short + await user.type(repeatPasswordInput, 'pass'); + await user.click(submitButton); + + expect(await screen.findByText(/Password must be at least 8 characters long/i)).toBeInTheDocument(); + expect(fetchMock).not.toHaveBeenCalled(); // Verify fetch was not called + }); + + it('shows an error message if passwords do not match', async () => { + const user = userEvent.setup(); + render(); + const usernameInput = screen.getByLabelText(/^Username$/i); + const passwordInput = screen.getByLabelText(/^Password$/i); + const repeatPasswordInput = screen.getByLabelText(/Repeat Password/i); + const submitButton = screen.getByRole('button', { name: /Create Account/i }); + + await user.type(usernameInput, 'testuser'); + await user.type(passwordInput, 'password123'); + await user.type(repeatPasswordInput, 'password456'); // Mismatch + await user.click(submitButton); + + expect(await screen.findByText(/Passwords do not match/i)).toBeInTheDocument(); + expect(fetchMock).not.toHaveBeenCalled(); // Verify fetch was not called + }); +}); diff --git a/attached_assets/Pasted--pages-view-viewid-js-6-25-value-TypeError-Cannot-read-properties-of-null-reading-value-1745393564971.txt b/attached_assets/Pasted--pages-view-viewid-js-6-25-value-TypeError-Cannot-read-properties-of-null-reading-value-1745393564971.txt new file mode 100644 index 0000000..6b433ae --- /dev/null +++ b/attached_assets/Pasted--pages-view-viewid-js-6-25-value-TypeError-Cannot-read-properties-of-null-reading-value-1745393564971.txt @@ -0,0 +1,26 @@ +⨯ pages/view/[viewid].js (6:25) @ value + ⨯ TypeError: Cannot read properties of null (reading 'value') + at View (webpack-internal:///./pages/view/[viewid].js:21:28) + at renderWithHooks (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5658:16) + at renderIndeterminateComponent (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5732:15) + at renderElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5957:7) + at renderNodeDestructiveImpl (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6115:11) + at renderNodeDestructive (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6087:14) + at finishClassComponent (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5688:3) + at renderClassComponent (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5696:3) + at renderElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5954:7) + at renderNodeDestructiveImpl (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6115:11) + at renderNodeDestructive (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6087:14) + at renderNode (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6270:12) + at renderHostElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5642:3) + at renderElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5963:5) + at renderNodeDestructiveImpl (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6115:11) { + page: '/view/A5F0A2E2' +} + 4 | import updateInterest, { updateAllowance } from "../../src/updateInterest"; + 5 | +> 6 | const View = ({account: {value}, transactions}) => { + | ^ + 7 | return ( + 8 | + 9 | \ No newline at end of file diff --git a/attached_assets/Pasted--pages-view-viewid-js-6-25-value-TypeError-Cannot-read-properties-of-null-reading-value-1745393567711.txt b/attached_assets/Pasted--pages-view-viewid-js-6-25-value-TypeError-Cannot-read-properties-of-null-reading-value-1745393567711.txt new file mode 100644 index 0000000..6b433ae --- /dev/null +++ b/attached_assets/Pasted--pages-view-viewid-js-6-25-value-TypeError-Cannot-read-properties-of-null-reading-value-1745393567711.txt @@ -0,0 +1,26 @@ +⨯ pages/view/[viewid].js (6:25) @ value + ⨯ TypeError: Cannot read properties of null (reading 'value') + at View (webpack-internal:///./pages/view/[viewid].js:21:28) + at renderWithHooks (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5658:16) + at renderIndeterminateComponent (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5732:15) + at renderElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5957:7) + at renderNodeDestructiveImpl (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6115:11) + at renderNodeDestructive (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6087:14) + at finishClassComponent (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5688:3) + at renderClassComponent (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5696:3) + at renderElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5954:7) + at renderNodeDestructiveImpl (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6115:11) + at renderNodeDestructive (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6087:14) + at renderNode (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6270:12) + at renderHostElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5642:3) + at renderElement (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:5963:5) + at renderNodeDestructiveImpl (/home/runner/workspace/node_modules/react-dom/cjs/react-dom-server.browser.development.js:6115:11) { + page: '/view/A5F0A2E2' +} + 4 | import updateInterest, { updateAllowance } from "../../src/updateInterest"; + 5 | +> 6 | const View = ({account: {value}, transactions}) => { + | ^ + 7 | return ( + 8 | + 9 | \ No newline at end of file diff --git a/attached_assets/content-1745392924501.md b/attached_assets/content-1745392924501.md new file mode 100644 index 0000000..5fa778b --- /dev/null +++ b/attached_assets/content-1745392924501.md @@ -0,0 +1,118 @@ +Menu + +Using App Router + +Features available in /app + +Using Latest Version + +15.3.1 + +Using App Router + +Features available in /app + +Using Latest Version + +15.3.1 + +[Docs](https://nextjs.org/docs) [Errors](https://nextjs.org/docs) Text content does not match server-rendered HTML + +# Text content does not match server-rendered HTML + +## [Why This Error Occurred](https://nextjs.org/docs/messages/react-hydration-error\#why-this-error-occurred) + +While rendering your application, there was a difference between the React tree that was pre-rendered from the server and the React tree that was rendered during the first render in the browser (hydration). + +[Hydration](https://react.dev/reference/react-dom/client/hydrateRoot) is when React converts the pre-rendered HTML from the server into a fully interactive application by attaching event handlers. + +### [Common Causes](https://nextjs.org/docs/messages/react-hydration-error\#common-causes) + +Hydration errors can occur from: + +1. Incorrect nesting of HTML tags +1. `

` nested in another `

` tag +2. `

` nested in a `

` tag +3. `