/notes-app

Primary LanguageTypeScriptMIT LicenseMIT

๐Ÿ“ Notes App

๐Ÿ‘‹ Introduction

Welcome to the Notes App, a simple and user-friendy application for managing notes. The application is powered by Wasp, a fullstack React, NodeJS, and Prisma framework.

โญ Features

  • ๐Ÿ” Authentication: Securely register for and access your account
  • โœ๏ธ Create notes: Easily compose and save your thoughts
  • โœ… Mark completed: Indicate task completion with a simple checkbox
  • ๐Ÿ—‘๏ธ Delete notes: Seamlessly remove unwanted notes
  • ๐Ÿ’พ Auto-save notes: Enjoy peace of mind as notes are saved automatically

๐Ÿš€ Getting Started

  1. Install Wasp
  2. Clone the repository: https://github.com/xsarahyu/notes-app.git
  3. Navigate to the app directory: cd app
  4. Run the server: wasp start db
  5. Run the client (in a new terminal): cd app โžก๏ธ wasp start
  6. The app is now live at http://localhost:3000!

๐Ÿ—‚๏ธ (Relevant) File Structure

The Wasp template provides many features that are irrelevant to the notes app, like Stripe payments, Google Analytics, and more. The following are the components and pages we utilized:

app/src
โ”œโ”€โ”€ client
โ”‚   โ”œโ”€โ”€ admin
โ”‚   โ”‚   โ”œโ”€โ”€ common
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ Loader
โ”‚   โ”‚   โ”‚   |   โ”œโ”€โ”€ index.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ components
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ DarkModeSwitcher.tsx
|   |   |   โ”œโ”€โ”€ Header.tsx
|   |   |   โ”œโ”€โ”€ Sidebar.tsx
|   |   |   โ”œโ”€โ”€ SidebarLinkGroup.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ layout
โ”‚   โ”‚   โ”‚   โ”œโ”€โ”€ DefaultLayout.tsx
โ”‚   โ”œโ”€โ”€ app
โ”‚   โ”‚   โ”œโ”€โ”€ AccountPage.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ DemoAppPage.tsx
โ”‚   โ”œโ”€โ”€ auth
โ”‚   โ”‚   โ”œโ”€โ”€ authWrapper.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ LoginPage.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ SignupPage.tsx
โ”‚   โ”œโ”€โ”€ components
โ”‚   โ”‚   โ”œโ”€โ”€ AppNavBar.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ DropdownUser.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ UserMenuItems.tsx
โ”‚   โ”œโ”€โ”€ hooks
โ”‚   โ”‚   โ”œโ”€โ”€ useColorMode.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ useLocalStorage.tsx
โ”‚   โ”œโ”€โ”€ landing-page
โ”‚   โ”‚   โ”œโ”€โ”€ contentSections.tsx
โ”‚   โ”‚   โ”œโ”€โ”€ LandingPage.tsx
โ”‚   โ”œโ”€โ”€ App.tsx
โ”‚   โ”œโ”€โ”€ Main.css
โ”œโ”€โ”€ server
โ”‚   โ”œโ”€โ”€ auth
โ”‚   โ”‚   โ”œโ”€โ”€ setUsername.tsx
โ”‚   โ”œโ”€โ”€ actions.ts
โ”œโ”€โ”€ shared
โ”‚   โ”œโ”€โ”€ constants.ts



๐Ÿงช Testing

Unit & Integration Tests โ€” Using Vitest

To run Vitest tests locally:

  1. Navigate to the app directory: cd app
  2. Install dependencies: npm install
  3. Run the tests: npm run test-ui (this is a script that we made, it runs all tests and generates test coverage)
  4. You should now see test coverage in your terminal, and a browser window should open with the Vitest UI, which includes the coverage as well!

๐Ÿ‘‰ DarkModeSwitcher component

Test #1: The DarkModeSwitcher component is a UI switcher element designed to toggle between light and dark color modes. The test ensures that the switching functionality works as expected.

test('DarkModeSwitcher toggles color mode correctly', () => {
const { container } = render(<DarkModeSwitcher />);
const switcher = container.querySelector('label');
// Initial state: light mode
expect(switcher).toHaveClass('bg-stroke');
// Click on the switcher to toggle mode
fireEvent.click(switcher);
// Expect it to have 'bg-primary' class after clicking
expect(switcher).toHaveClass('bg-primary');
// Click again to revert back
fireEvent.click(switcher);
// Expect it to have 'bg-stroke' class after clicking again
expect(switcher).toHaveClass('bg-stroke');
});

๐Ÿ‘‰ DemoAppPage

Test #1: Ensures that users can create a new note by entering text into the input field and clicking the "Add Note" button.

describe('Testing Note CRUD', () => {
// TEST 1: Confirm user sees text default "Remember to..." in text field, can add note, see newly created note
test('Testing creating new note', async () => {
//Creating Mocked note
const mocknotes = [
{
id: "1",
description: "test todo 1",
createdAt: new Date(),
time: "4:20",
isDone: true,
userId: 1,
},
];
renderInContext(<DemoAppPage />);
//To ensure you see the text for new note
let input = screen.getByPlaceholderText('Remember to...');
fireEvent.change(input, { target: { value: mocknotes[0].description } });
input = screen.getByDisplayValue(mocknotes[0].description);
expect(input).toBeInTheDocument();
//Creating new note
const addButton = screen.getByText('Add Note');
fireEvent.click(addButton);
//This is simulating creating a server call when the note is created
mockQuery(getAllTasksByUser, mocknotes);
// Wait for asynchronous operations, then make assertions
await waitFor(() => {
//Confirming Mock note was added
let newnote = screen.getByText(mocknotes[0].description)
expect(newnote).toBeInTheDocument();
});
});

Test #2: Verifies that users can check and uncheck a note by toggling its checkbox.

test('Testing note checkbox feature', async () => {
//Creating Mocked note
const mocknotes = [
{
id: "1",
description: "test todo 1",
createdAt: new Date(),
time: "4:20",
isDone: false,
userId: 1,
},
];
renderInContext(<DemoAppPage />);
//Creating a fake server call to test
mockQuery(getAllTasksByUser, mocknotes);
// Wait for asynchronous operations, then make assertions
await waitFor(() => {
// Assert that the checkbox is not checked
expect(screen.getByRole("checkbox")).not.toBeChecked();
const checkbox = screen.getByRole("checkbox");
console.log(checkbox);
//Clicking checkbox
fireEvent.click(checkbox)
// Wait for state to update. Check checkbox is checked
waitFor(() => {
expect(checkbox).toBeChecked();
});
});
});

Test #3: Ensures that users can delete a note by clicking the delete button associated with it.

test('Testing deleting note', async () => {
// Render the component
renderInContext(<DemoAppPage />);
// Create mock notes
const mocknotes = [
{
id: "1",
description: "Test todo 1",
createdAt: new Date(),
time: "4:20",
isDone: false,
userId: 1,
}
];
//Creating a fake server call to test
mockQuery(getAllTasksByUser, mocknotes);
// Wait for note to appear in the UI
await waitFor(() => {
//Ensure note is created
expect(screen.getByText('Test todo 1')).toBeInTheDocument();
// Simulate the deletion action
const deleteButton = screen.getByTitle('Remove task')
fireEvent.click(deleteButton);
//Check that it was deleted
waitFor(() => {
expect(screen.queryByText('Test todo 1')).not.toBeInTheDocument();
});
});
});
})

Test #4: Ensures that the default text elements are displayed correctly when the page is loaded.

test('Verify default text displayed', async () => {
renderInContext(<DemoAppPage />);
//Button text "Add Note" appears
const addButton = screen.getByText('Add Note');
expect(addButton).toBeInTheDocument();
//Iput text default "Remember to.." appears
let input = screen.getByPlaceholderText('Remember to...');
expect(input).toBeInTheDocument();
})

๐Ÿ‘‰ AppNavBar component

Test #1: Ensures that the navigation links in the navbar are rendered correctly.

test('Testing NavBar Content', async () => {
renderInContext(<AppNavBar />);
// Test 2: see the text on the page "Documentation"
const docNavBarItem = await screen.findByText('Documentation');
// Checked if it found an object/HTML Element whose text says 'Documentation '
expect(docNavBarItem).toBeInTheDocument();
});

๐Ÿ‘‰ LandingPage

Test #1: Ensures that the header renders correctly.

test('Renders header', async () => {
await waitFor(() => {
const header = screen.getByText('RC ASCEND')
const menuButton = screen.getByRole('button', {name: 'Open main menu'})
expect(header).toBeInTheDocument
expect(menuButton).toBeInTheDocument
})
})

Test #2: Ensures that the hero section renders correctly.

test('Renders hero section', async () => {
await waitFor(() => {
const hero1 = screen.getByText('Discover the ultimate', { selector: 'h1' })
const hero2 = screen.getByText('Notes app', { selector: 'span' })
const getStartedButton = screen.getByRole('link', {name: 'Get Started'})
expect(hero1).toBeInTheDocument()
expect(hero2).toBeInTheDocument()
expect(getStartedButton).toBeInTheDocument()
})
})

Test #3: Ensures that the features section renders correctly.

test('Renders features section', async () => {
await waitFor(() => {
const createNotes = screen.getByText('Create Notes')
const markCompleted = screen.getByText('Mark Completed')
const deleteNotes = screen.getByText('Delete Notes')
const autoSave = screen.getByText('Auto-Save')
expect(createNotes).toBeInTheDocument
expect(markCompleted).toBeInTheDocument
expect(deleteNotes).toBeInTheDocument
expect(autoSave).toBeInTheDocument
})
})

Test #4: Verifies that clicking the "Get Started" button leads to the signup page.

test('Get Started button leads to signup page', async() => {
const history = createMemoryHistory()
renderInContext(
<Router history={history}>
<LandingPage />
</Router>
)
const getStartedButton = screen.getByRole('link', {name: 'Get Started'})
fireEvent.click(getStartedButton)
await waitFor(() => {
expect(history.location.pathname).toBe('/signup')
})
})

End-to-End Tests โ€” Using Cypress

To run Cypress tests locally:

  1. Navigate to the app directory: cd app
  2. Install Linux dependencies: sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
  3. Install other dependencies: npm install
  4. Run the server: wasp start db
  5. Run the client (in a new terminal): cd app โžก๏ธ wasp start
  6. Open the Cypress test runner (in a new terminal): cd app โžก๏ธ npx cypress open

The following steps occur in the Cypress test runner:

  1. In the test runner, select "E2E Testing."
  2. You'll be prompted to choose a browser to run the tests in. Select "Start E2E Testing in Electron."
  3. Cypress will open a new window containing a list of all the end-to-end tests.
  4. Click on a test file to run it.
  5. The selected test will begin execution. You'll see each step being performed in real-time within the Cypress GUI.
  6. If the test passes, the screen will turn green. If it fails, the screen will turn red, indicating which step failed.
  7. To run other test files, navigate back to the main page by selecting the "Specs" label in the sidebar.
  8. From the main page, you can select and run other test files one by one.

๐Ÿ‘‰ account_settings

The account_settings test suite verifies the functionality of accessing the account settings page.

describe('The AccountSettings Test', () => {
it('test that you can click the account settings page', function () {
//Created a Custom Command: Goes to sign up page and creates a user
cy.createUser()
// View the app as if it were a computer screen
cy.viewport(1280, 800)
// Opens the menu and keeps the dropdown menu open
cy.get('#dropdownmenu').click({force: true});
// clicks the account settings and keeps the dropdown menu open
cy.get('#accountsettingsbutton').click({force: true});
// Test to see if the current url equals the accoount page
cy.url().should('eq', 'http://localhost:3000/account')
})
})

๐Ÿ‘‰ documentation_page

The documentation_page test suite verifies the functionality of accessing the documentation page.

describe('The Documentation Page', () => {
it('test that you can click the documentation page', function () {
//Created a Custom Command: Goes to sign up page and creates a user
cy.createUser()
// View the app as if it were a computer screen
cy.viewport(1280, 800)
// Clicks on the Documentation button in the navbar
cy.get('a').contains('Documentation').click()
// Allows the test to bypass a single origin url
cy.origin('https://github.com/xsarahyu/notes-app', () => {
// Test to see if the current url equals the document page
cy.url().should('eq', 'https://github.com/xsarahyu/notes-app')
})
})
})

๐Ÿ‘‰ login_page

The login_page test suite verifies the functionality of logging in.

describe('The Login Page', () => {
it('test that you can login to the app', function () {
//Created a Custom Command: Goes to sign up page and creates a user
cy.createUser()
cy.visit('/login')
// Access input element "activeUser" with properties usr and pwd
cy.get('@activeUser').then(activeUser => {
//Input username w/ object
cy.get('input[name=username]').type(activeUser.username)
// Input password w/ object
cy.get('input[name=password]').type(`${activeUser.password}{enter}`)
// we should be redirected to /demo-app
cy.url().should('include', '/demo-app')
});
})
})

๐Ÿ‘‰ notes

The notes test suite verifies the functionality of adding, crossing off, and deleting a note.

describe('The Notes Test', () => {
beforeEach(() => {
// Custom command: Go to sign up page and create a user
cy.createUser()
// View the app on 1280x800 size screen
cy.viewport(1280, 800)
// Click the demo app
cy.get('a[href="/demo-app"]').click()
// Assert that URL is '/demo-app'
cy.url().should('eq', 'http://localhost:3000/demo-app')
// Type a note
cy.get('#description').click().type('Test note')
// Click the button to add the note
cy.contains('Add Note').click()
})
it('Can make a note', function () {
// Assert that notes section contains 'Test note'
cy.get('.space-y-4').should('contain', 'Test note')
})
it('Can check off a note', function () {
// Click the checkbox
cy.contains('Test note').siblings('input[type="checkbox"]').check()
// Assert that checkbox is checked
cy.contains('Test note').siblings('input[type="checkbox"]').should('be.checked')
// Assert that note is crossed out
cy.contains('Test note').should('have.class', 'line-through')
})
it('Can delete a note', function() {
// Click the delete button
cy.get('button[class="p-1"]').click()
// Assert that note is gone
cy.contains('Test note').should('not.exist')
})
})

๐Ÿ‘‰ logout

The logout test suite verifies the functionality of logging out.

describe('The Logout Test', () => {
it('test that you can click logout and it will log the user out', function () {
//Created a Custom Command: Goes to sign up page and creates a user
cy.createUser()
// View the app as if it were a computer screen
cy.viewport(1280, 800)
// Opens the menu and keeps the dropdown menu open
cy.get('#dropdownmenu').click({force: true});
// clicks the logoutbutton and keeps the dropdown menu open
cy.get('#logoutbutton').click({force: true});
// Test to see if the current url equals the document page
cy.url().should('eq', 'http://localhost:3000/login')
})
})

๐Ÿ‘‰ signup_page

The signup_page test suite validates the functionality of the sign-up process.

describe('The Sign-up Page', () => {
it('allows a user to sign up', () => {
//Created a Custom Command: Goes to sign up page and creates a user
cy.createUser()
// Check if the user is redirected to the expected page after signing up
cy.url().should('include', '/demo-app');
// Check if the sign-up message or confirmation is displayed
cy.contains('Add Note').should('be.visible');
})
})