diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index a96a53a..acc5c04 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,6 +4,7 @@ on: push: branches: - main + jobs: build: @@ -26,7 +27,11 @@ jobs: - name: Build Frontend run: | cd frontend + npm run test npm run build + env: + VITE_FRONTEND_USERNAME: ${{secrets.FRONTEND_USERNAME}} + VITE_FRONTEND_PASSWORD: ${{secrets.FRONTEND_PASSWORD}} - name: Set up Java for Backend uses: actions/setup-java@v4 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..5e632ca --- /dev/null +++ b/LICENSE @@ -0,0 +1,25 @@ +# Licensing + +This project, "Timetabling for VIT," was developed for the Victorian Institute of Technology (VIT) as part of an industry-based project facilitated by the University of Melbourne. + +## Copyright Notice + +Copyright © 2024 Victorian Institute of Technology (VIT). All rights reserved. + +All intellectual property rights in the deliverables for this project are owned by the University of Melbourne and have been assigned to the Victorian Institute of Technology (VIT) under the terms of the Student Assignment and License Deed Poll. This assignment grants VIT exclusive rights to the code, documentation, and any related intellectual property created for this project. + +## Use and Distribution Restrictions + +The use, reproduction, modification, or distribution of this project's source code or any related deliverables is prohibited without the express written permission of the Victorian Institute of Technology. Unauthorized use of this project, in whole or in part, may result in legal action. + +## Open-Source Components + +This project may include open-source libraries or frameworks. Each such component retains its original license, and users are required to comply with the terms of each respective license. Please consult the documentation of each external component for detailed licensing information. + +## Confidentiality + +All confidential information, proprietary code, and other non-public materials remain the exclusive property of the Victorian Institute of Technology. By accessing or using this project, you agree to abide by these terms and to maintain the confidentiality of the project’s proprietary information. + +--- + +For questions or permissions, please contact the Victorian Institute of Technology. \ No newline at end of file diff --git a/README.md b/README.md index 77c4aa7..a911fec 100644 --- a/README.md +++ b/README.md @@ -7,63 +7,385 @@
+## Demo -## Dependencies +https://vit-timetabling-230c1835ad5a.herokuapp.com -Before cloning and attempting to run this code, you will need: +
+ + +## Features + +
+ Login + + Users can log in with a pre-set username and password provided by Team Jet Edge. +
+
+ Welcome Page + + After logging in, users are presented with two options: + - Generate Timetable: Starts the process of uploading a student enrolment Excel file to generate a new timetable. + - Modify Timetable: Allows users to edit existing timetables by navigating to the Timetable Page. +
+
+ Upload File + + - Selecting “Generate Timetable” on the Welcome Page brings up the "Upload File" pop-up window. + - Click **UPLOAD FILE** at the top to select and upload the student enrolment Excel file. + - After uploading, click **PROCEED** at the bottom to move to the Information Page. +
+
+ Information Page + + On the Information Page, users can edit room and unit information as needed. + - Certain columns (Campus, Course, Unit Code, Enrolled Students) in the Unit Table are parsed directly from the uploaded Excel file and cannot be modified. + - Right-clicking on a cell opens a context menu for more options, such as inserting or deleting rows. + - Editing the information page can be done by copying and pasting data from an existing pre-formatted Excel file. + +Once editing is complete, click **NEXT** at the bottom right to proceed to the Generate Timetable page. +
+
+ Generate Timetable + + - Click **GENERATE TIMETABLE** to start the timetabling process. + - The processing icon will indicate progress and return to its original state once generation is complete. + - Click **NEXT** at the bottom right to move to the Timetable Page. +
+
+ Display and Modify Timetable + + - Users can view the timetable by campus by selecting campus names on the left-hand side of the page. + - Drag-and-Drop Editing: Users can drag and drop units to adjust room and time allocations as needed. Click **SAVE CHANGES** to apply edits to the database. + - **Note**: The system will display a warning if: + - A unit overlaps with another time slot (“OVERLAPPED”). + - A unit is assigned outside a designated room (“ASSIGN ACTIVITIES TO ROOMS ONLY”). +
+
+ Download Timetable + + If no further changes are needed, click **DOWNLOAD TIMETABLE** at the bottom to download the campus timetable as a ZIP file. + + **Note**: Each campus has a ZIP file containing three timetables, one for each course offered at that campus. Therefore, to download all 12 timetables (4 campuses × 3 courses per campus = 12 timetables), the user only needs to download four ZIP files, one per campus. +
+ +
+ +## Documentation + +
+ User Stories + + | Issue key | Epic Name | User Story Name | User Story | Priority | +| --------- | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| KAN-51 | Automate Timetable Generation | Add workdays to algorithms | As a student support team member,
I want the scheduling algorithm to work across 5 working days instead of just one day,
so that we can create timetables for the entire week, offering more flexibility and better scheduling options for students. | High | +| KAN-41 | Automate Timetable Generation | Connect to Azure database | As a Student Service team member,
I want to store semester and campus information for later retrieval,
So that I can view and edit them later down the line without having to re-enter all the data, which saves time and effort. | High | +| KAN-32 | Automate Timetable Generation | Constraint 1: no students have overlapping units | As a student service team member,
I want to implement a constraint that ensures no student has overlapping classes
so that students can attend all their enrolled classes without any scheduling conflicts. | Highest | +| KAN-45 | Automate Timetable Generation | Constraint 2: no overlapping units are in the same room | As a student service team member,
I want to implement a constraint that ensures no two classes are scheduled in the same classroom at the same time
so that there are no conflicts or double-bookings in the timetable, ensuring each class has a dedicated space. | Highest | +| KAN-34 | Automate Timetable Generation | Constraint 3: ensure room capacity is not less than allocated unit size | As a student service team member,
I want to implement a constraint that ensures the room size is greater than or equal to the class size
so that all scheduled classes have adequate space for all enrolled students. | Highest | +| KAN-52 | Automate Timetable Generation | Constraint 4: lab units prefer lab rooms | As a student service team member,
I want to implement a constraint that ensures lab units are allocated to lab rooms whenever possible
so that practical classes are held in appropriate environments conducive to hands-on learning. | Highest | +| KAN-53 | Automate Timetable Generation | Constraint 5 (Room Availability Constraint): Filter the room in Front End (if a room is labelled not available, do not sent it to the backend). | As a student service team member,
I want to label the rooms as available or not,
so that only available rooms are scheduled. | High | +| KAN-56 | Automate Timetable Generation | Front end persistence using indexedDB | As a Student Service Team member,
I want all input data to be saved at least until I have finished inputting all data necessary for generating the timetable
so that I can input all the data for the schedule process. | High | +| KAN-57 | Automate Timetable Generation | Store and get info from database | As a Student Service Team member,
I want to store semester and campus information for later retrieval,
so that I can view and edit them later down the line without having to re-enter all the data, which saves time and effort. | Medium | +| KAN-92 | General | Improve UI/UX design on frontend | As a Student Service Team member,
I want the product to be aesthetically feasible,
so that it would match with other VIT websites | Lowest | +| KAN-91 | General | Skip button straight to timetable page | As a Student Service Team member,
I want to be able to modify my previously generated timetable right away
so that I can save time | High | +| KAN-77 | Security | Basic Authentication at backend | As a Student Service Team member,
I want to ensure that access to the backend is secured using Basic Authentication
so that only authorized users can interact with the system. | High | +| KAN-76 | Security | Integrate with backend Basic Auth + reroute to login page | As a Student Service Team member,
I want the application to be secure
so that only authorised personnel can access the website to use its functionalities and view VIT’s timetables. | High | +| KAN-37 | Store Timetable Input Data | Allow classes to take up more than 1 timeslot | As a Student Service Team member,
I want to be able to make timetables with class durations of varying length
so that I can have flexibility in the classes I plan to have at my institution, and still be able to generate a timetable for them. | Highest | +| KAN-26 | Store Timetable Input Data | Build campus data input page | As a member of the Student Service Team,
I want to input information about each campus
so that the timetables I want to generate is specific to each campus. | Medium | +| KAN-27 | Store Timetable Input Data | Build classroom/lab data input page | As a member of the Student Service Team,
I want to input information about each building and classroom
so that lessons can be allocated to a physical location. | Medium | +| KAN-25 | Store Timetable Input Data | Build course data input page | As a member of the Student Service Team,
I want to input information about each course
so that the timetable for students enrolled in each course is different. | Medium | +| KAN-24 | Store Timetable Input Data | Build file upload page for enrolment info | As a member of the Student Service Team,
I want timetables to be generated using the enrolment information of the current semester
so that classroom/space allocation will be efficient and adjust dynamically between semesters. | Highest | +| KAN-36 | Store Timetable Input Data | Client side page routing | As a Student Service Team member,
I want lower load time when navigating between pages,
so that I can get my tasks done faster. | Medium | +| KAN-39 | Store Timetable Input Data | Connect Quarkus backend with React frontend | As a Student Service Team member,
I want to enter data and for it to be processed to generate a timetable.
so that I can have an automated and quick way to create timetables, without loads of manual labour. | Medium | +| KAN-20 | Store Timetable Input Data | Design data Input UI | As a member of the Student Service Team,
I want the system to have an user-friendly data input interface,
so that I can submit information for generating timetables quickly. | High | +| KAN-21 | Store Timetable Input Data | Setup database | As a Student Service Team member,
I want to store semester and campus information for later retrieval,
So that I can view and edit them later down the line without having to re-enter all the data, which saves time and effort. | Highest | +| KAN-43 | Store Timetable Input Data | Take into account classrooms and labs priority constraint | As a Student Service Team member,
I want to prioritize the allocation of specific classrooms and labs based on their suitability for certain courses
so that the timetable reflects the optimal use of resources, ensuring that specialized spaces are reserved for classes that require them. | High | +| KAN-59 | Store and get info from database | Store generated timetable to database | As a Student Service Team member,
I want to store semester and campus information for later retrieval,
so that I can view and edit them later down the line without having to re-enter all the data, which saves time and effort.
This is not completed for Sprint 1 due to our change of backend framework. This will be moved to Sprint 2. | Medium | +| KAN-58 | Store and get info from database | Store student enrolment info from .xlsx file | As a Student Service Team member,
I want to store semester and campus information for later retrieval,
So that I can view and edit them later down the line without having to re-enter all the data, which saves time and effort. | Medium | +| KAN-69 | Testing and Deployment | Backend deployment | As a Student Service Team member,
I want the backend of the application to be properly deployed
so that it reliably processes data and supports frontend interactions. | High | +| KAN-62 | Testing and Deployment | Backend Unit Tests | As a developer,
I want to thoroughly test the backend functions and modules,
so that I can catch bugs early, validate the behaviour of APIs, and verify the correctness of business logic, ensuring that all unit tests pass when individual components are tested in isolation. | High | +| KAN-87 | Testing and Deployment | Connect deployed frontend to the deployed backend | As a Student Service Team member,
I want the application to generate timetables using a deployed backend
so that I don’t have to run the backend locally. | Highest | +| KAN-68 | Testing and Deployment | Frontend deployment | As a Student Service Team member,
I want the front end of the application to be properly deployed
so that I can access and interact with the system’s interface seamlessly. | High | +| KAN-65 | Testing and Deployment | Frontend Unit Tests | As a Student Service Team member,
I want the application UI to be free of bugs
so that I can work efficiently. | Medium | +| KAN-82 | Testing and Deployment | Integration Tests | As a Student Service Team member,
I want the application to be able to communicate seamlessly with the backend
so that I can work coherently. | Medium | +| KAN-83 | Testing and Deployment | Security Tests | As a backend developer,
I want the backend to enforce secure access to endpoints,
so that unauthorized users cannot access sensitive data or system functionalities. | Medium | +| KAN-94 | Testing and Deployment | System Tests | As a system tester,
I want to test the entire timetable system comprehensively, including functional, usability, and performance aspects,
so that I can ensure it meets the specified requirements and works seamlessly in real-world scenarios. | High | +| KAN-95 | Testing and Deployment | User Acceptance Tests | As a student service team member or administrator,
I want to interact with the timetable system in a real-world scenario,
so that I can verify that the system meets my needs, is easy to use, and satisfies all business requirements. | High | +| KAN-84 | View and Edit Timetable | Change website tab name to "Timetabling for VIT" | As a Student Service Team member,
I want the application to have a consistent VIT theme
so that this application is made for VIT. | Low | +| KAN-49 | View and Edit Timetable | Display Gantt Chart for received data | As a Student Service Team member,
I want the auto-generated timetable to have a clear display format
so that I can easily view and make modifications. | High | +| KAN-74 | View and Edit Timetable | Ensure database only stores one timetable for each campus at all times | As a Student Service Team member,
I want to only have one timetable for each campus at all times, i.e. each time I generate a new timetable for a campus, any existing timetable for that particular campus should be removed
So that I don’t have to sift through a lot of timetables and always be focused on just one | High | +| KAN-75 | View and Edit Timetable | Fix database, prevent leaking large input files | As a Student Service Team member,
I want to have all the data I enter into the system saved
So that I can retrieve these data later | High | +| KAN-50 | View and Edit Timetable | Handle download timetables | As a Student Service Team member,
I want to download the auto-generated timetable(s)
so that I view them locally on my own PC and set up timetables for my students. | High | +| KAN-60 | View and Edit Timetable | Handle dynamic routing with dynamic campus inputs | As a Student Service Team member,
I want to view the timetable of 1 campus per Gantt chart
so that any changes in room/time allocation I make will not clash with other classes happening on the same campus. If more campuses are added in the future, the web-application should be dynamic and display timetables for those new campuses in new tabs as well. | High | +| KAN-71 | View and Edit Timetable | Modify DB schema to allow storage of timetables by campus | As a Student Service Team member,
I want to view timetables by campus
So that I can know the classes happening for each campus during the semester and modify accordingly since classes can’t be changed across campuses. | High | +| KAN-86 | View and Edit Timetable | Remove the "Building" tab from input spreadsheets page | As a Student Service Team member,
I want the UI to not contain unnecessary elements
so that I can focus on my tasks. | Low | +| KAN-80 | View and Edit Timetable | Save unit drag-and-drop changes to database | As a Student Service Team member,
I want to drag and drop to modify existing timetables and have those changes saved
So that when I return to the timetable, it always has the latest changed applied, and I can keep modifying from there | High | +| KAN-73 | View and Edit Timetable | Split user input by campus before sending to backend | As a Student Service Team member,
I want the application to generate 1 timetable for each campus
so that each campus is constrained separately by their available buildings. | High | +| KAN-72 | View and Edit Timetable | Update frontend API method to match latest backend | As a Student Service Team member,
I want the process of communicating with the backend to work seamlessly,
so that I can generate a timetable from my input. | High | +| KAN-85 | View and Edit Timetable | Update pages to show loading state whilst processing data | As a Student Service Team member,
I want the application UI to feel interactive when it’s loading,
so that I know I am waiting on something to be done. | High | +
+
+ + Motivational Model +
+ +
+
+ + Domain Model +
+ +
+
+ + Flow Diagram +
+ +
+
+ + Architecture Diagram +
+ +
+
+ + Database ER Diagram +
+ +
+ +
+ + +## System requirements + +This timetabling web application is built with the following coding and deployment environment configurations (please find setup guides linked for each): + +- [Windows 11](https://www.microsoft.com/en-us/software-download/windows11) (ideal): the project was developed solely on Windows 11 machines. + + - If you use another operating system, there may be slight differences to configuration, but this will be beyond the scope of this document. + +- [Java Development Kit](https://www.oracle.com/au/java/technologies/downloads/) (JDK) version `22.0.2`: language for backend development. + + - Minimum requirement: JDK 17+ with `JAVA_HOME` configured appropriately on your machine. + +- [Apache Maven version](https://maven.apache.org/download.cgi) `4.0.0`: build-automation and project management tool for Java-based projects. + +- [PostgreSQL version](https://www.postgresql.org/download/) `16.4`: database for timetable storage. + + - Offers [pgAdmin](https://www.pgadmin.org/) for data administration and further manipulation (comes with standard download). + +- [Node.js](https://nodejs.org/en/download/package-manager) version `20.17.0`: JavaScript runtime environment for frontend development. + +- [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) version `2+`: version control and collaborative development. + + - Git preserves backwards compatibility very well, any recent version should work fine, too. + +- [Heroku CLI](https://devcenter.heroku.com/articles/heroku-cli) version `7.0.0`: deployment and workflow management. + +- For the versions of plugins, packages, and frameworks used, they are specified in the configuration files of the repository and you do not need to perform any installations or take any additional action. Please refer to: + + - The `jetedge/backend/pom.xml` file located in the repository for backend dependencies. + + - The `jetedge/frontend/package.json` file located in the repository for frontend dependencies. + +
+ +## Installation guide + +### Repository Setup + +If you wish to build upon the codebase, please first ensure your machine meets the prerequisites outlined in the previous section, then follow the steps below to create a local coding environment. -- Node.js: Required for the frontend development, which uses JavaScript and Node.js. -- Java 11 or later: Required for running the backend developed with the Quarkus framework. -- Maven: For managing Java dependencies and building the backend. -- PostgreSQL: The database used in this project, along with pgAdmin for database management. -- Git: For version control and cloning the repository. (Optional, repo can also be downloaded as .zip) -- Heroku CLI: To deploy and manage the application on Heroku. (Optional, for deployment only) +1. Navigate to the JetEdge GitHub repository and fork it. + + - Forking instructions: https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo + +2. In your terminal of choice, navigate inside the folder in which you wish to clone the forked repository. + +3. Clone the forked repository. +``` +git clone https://github.com/{your-github-name}/jetedge.git +``` +4. Navigate inside the cloned folder. +``` +cd jetedge +``` +5. Navigate inside the backend folder for backend setup. +``` +cd backend +``` +6. Create a `.env` file in `backend` folder with database and login information + +7. Navigate inside the frontend folder for frontend setup. +``` +cd frontend +``` +8. Create a `.env` file with the login information. + + - These constants are for testing purposes, please ensure they are consistent with the credentials in the backend/.env file. + +9. Install dependencies. +``` +npm install +``` +10. In preparation to run the code locally, please navigate inside `frontend` folder and replace all instances of the constant `REMOTE_API_URL` with `LOCAL_API_URL`. + +11. Run the frontend and backend simultaneously in development mode, with two separate terminal windows + +12. Run the frontend in development mode. +``` +cd jetedge/frontend +npm run dev +``` +13. Run the backend in development mode. +``` +cd jetedge/backend +./mvnw compile quarkus:dev +``` +If you wish to run the servers in production mode locally: + +1. Execute the following to build and run the frontend server in production mode. +``` +cd frontend +npm run build +npm run preview +``` +2. Execute the following to package and run the backend server in production mode. +``` +cd backend +./mvnw package +java -jar target/quarkus-app/quarkus-run.jar +``` + +### pgAdmin Setup + +If you wish to perform data administration tasks on the Azure cloud-based PostgreSQL database we provide, please follow the [guide](https://www.sqlshack.com/accessing-azure-database-for-postgresql-using-pgadmin/) to connect the cloud database with your local pgAdmin. + +### PostgreSQL Database Setup + +If you wish to set up your own PostgreSQL database, please read the steps below to help you get started. + +Firstly, for context, the JetEdge team used an Azure Student License for the duration of this project. The setup will vary depending on the type of license you have and we presume VIT will not be using the Student License. Therefore, we will provide an overview of how to set up your own database along with the resources you will find helpful. + +If you are considering to use Azure database for PostgreSQL, please check Azure’s pricing model here: https://azure.microsoft.com/en-au/pricing/details/postgresql/server/ + +Now if you decide to go ahead with setting up your own Azure PostgreSQL database, the first step is to create an account for the Azure portal, then perform database setup and deployment on Azure. This video provides a step-by-step setup guide for someone with a general license: https://www.youtube.com/watch?v=AsL7MI8b0m4 + +After this, please extract the datasource username, password, and JDBC url, and enter them into the .env. file located in the backend folder for the appropriate constants. + +This should complete the setup. Once you run the Quarkus backend server, the necessary tables in the database will be automatically set up and ready for use.
-## How to run the app +## Testing + +- Unit Testing + - [Frontend](tests/Unit%20Testing%20(Frontend).pdf) + - [Backend](tests/Unit%20Testing%20(Backend).pdf) +- [Integration Testing](tests/Integration%20Testing.pdf) +- [System Testing](tests/System%20Testing.pdf) +- [Security Testing](tests/Security%20Testing.pdf) +- [User Acceptance Testing](tests/User%20Acceptance%20Testing.pdf) + +
+ +## Deployment guidelines + +You can deploy this site to any hosting platform of your choice, but we have selected Heroku. To ensure smooth deployments and updates, follow the steps outlined below for managing the frontend and backend of the "Timetabling for VIT" application. + +
-### Running the frontend -Move into the frontend folder with: -`cd frontend` + Frontend Deployment -Install all the packages with (only needs to be done once): -`npm i` + - [Current Deployment URL](https://vit-timetabling-230c1835ad5a.herokuapp.com/) + - Process: The frontend is deployed as a Node.js app on Heroku. + - Update Instructions: + - Automatic Deployment: Push updates to the `main` branch on GitHub to automatically trigger the CI/CD pipeline for deployment on Heroku. + - Manual Deployment: If needed, log in to Heroku, access the frontend app, and deploy manually via the Heroku CLI. -Run in development mode with: -`npm run dev` +
+
-Build for production with: -`npm run build` + Backend Deployment -Run in production mode: -`npm run preview` + - [Current Deployment URL](https://jetedge-backend-e1eeff4b0c04.herokuapp.com/) + - Process: The backend is deployed as a Java app on Heroku. + - Update Instructions: + - Automatic Deployment: Push updates to the `main` branch on GitHub to automatically trigger the CI/CD pipeline for deployment on Heroku. + - Manual Deployment: If needed, log in to Heroku, access the backend app, and deploy manually via the Heroku CLI. -### Running the backend -Move into the backend folder with: -`cd backend` +
+
-Run in development mode with: -`./mvnw compile quarkus:dev` + CI/CD Pipeline -Package the project as a JAR file with (only needs to be done once): -`./mvnw package` + - Platform: GitHub Actions + - [Pipeline Link](.github/workflows/main.yml) + - Setup: The pipeline automates deployments for both frontend and backend. Pushing to the `main` branch triggers automatic deployment to Heroku. -Run in production mode: -`java -jar target/quarkus-app/quarkus-run.jar` +
+ +**Notes** +- Environment Variables: Ensure all required variables are updated in your local environment, GitHub Actions secrets, and Heroku app settings + - `FRONTEND_USERNAME`, `FRONTEND_PASSWORD`: Login details for accessing the website. + - `HEROKU_EMAIL`, `HEROKU_API_KEY`, `HEROKU_FRONTEND_APP_NAME`, `HEROKU_BACKEND_APP_NAME`: Heroku credentials for automated deployment. + - `QUARKUS_DATASOURCE_USERNAME`, `QUARKUS_DATASOURCE_PASSWORD`, `QUARKUS_DATASOURCE_JDBC_URL`: Database credentials for the backend. +- Error Handling: Review Heroku logs for any deployment or runtime issues. Logs are accessible via the Heroku dashboard or by running `heroku logs --tail` in the command line. +- Security: Keep all credentials secure by storing them in environment variables and avoid hard-coding sensitive information.
-## Tech Stack -- Frontend: JavaScript with Node.js -- Backend: Java with Quarkus -- Database: PostgreSQL -- CI/CD: GitHub Actions -- Deployment: Heroku +## Changelog + +### Changes in Sprint 3 (23 September — 20 October) + +- Removed deprecated student endpoints, renamed API endpoints for consistency https://github.com/hotungkhanh/jetedge/pull/55 + +- bugfix: sidebar now does not overflow into headers https://github.com/hotungkhanh/jetedge/pull/53 + +- Reformatted the “enrolment“ page, “Save Changes“ and “Download Timetable“ button will now always be visible in the “timetablemod“ page https://github.com/hotungkhanh/jetedge/pull/52 + +- ”Save Changes” now saves timetable’s changes into database https://github.com/hotungkhanh/jetedge/pull/51 + +- Deployed frontend now uses the deployed backend https://github.com/hotungkhanh/jetedge/pull/46 + +- Add a skip button to jump directly to the previously generated timetable https://github.com/hotungkhanh/jetedge/pull/44 + +- Disable buttons in “senddata“ when timetable is not yet generated, only enabling when backend has finished generating https://github.com/hotungkhanh/jetedge/pull/43 + +- Removed the “Building“ tab from the sidebar in “seminfo“ https://github.com/hotungkhanh/jetedge/pull/41 + +- Update tab name to “Timetabling for VIT“ https://github.com/hotungkhanh/jetedge/pull/39 + +- User authentication required for database access, routes are now private and login is required on frontend https://github.com/hotungkhanh/jetedge/pull/35 + +- Implement dynamic sidebar, able to update accordingly to campuses sent from backend. Timetable are now displayed in gantt-chart format https://github.com/hotungkhanh/jetedge/pull/32 + +### Changes in Sprint 2 (2 September — 22 September) + +- Increase transaction timeout limit, and removed the storage of students for faster transactions https://github.com/hotungkhanh/jetedge/pull/31 + +- Deploy frontend https://github.com/hotungkhanh/jetedge/pull/23 + +- Handle empty input in “seminfo“: an alert will pop up if input is empty while trying to solve a problem. Unit List in “seminfo/units“ are now also read-only https://github.com/hotungkhanh/jetedge/pull/13 + +- Frontend is now connected to a functional backend https://github.com/hotungkhanh/jetedge/pull/10 + +- Persistence in the frontend “seminfo“ page https://github.com/hotungkhanh/jetedge/pull/9 + +### Changes in Sprint 1 (12 August — 1 September) + +- Build file upload page for enrolment info https://github.com/hotungkhanh/jetedge/pull/4 + +- Build semester info data input page https://github.com/hotungkhanh/jetedge/pull/2
+ ## Contributors diff --git a/backend/pom.xml b/backend/pom.xml index 0dc9d4c..06f6f56 100644 --- a/backend/pom.xml +++ b/backend/pom.xml @@ -1,6 +1,5 @@ - + 4.0.0 org.acme @@ -56,6 +55,14 @@ io.quarkus quarkus-agroal + + io.quarkus + quarkus-smallrye-openapi + + + io.quarkus + quarkus-swagger-ui + io.quarkus quarkus-junit5 @@ -78,23 +85,23 @@ test - org.awaitility - awaitility + org.awaitility + awaitility test - org.assertj - assertj-core + org.assertj + assertj-core 3.26.3 test - io.quarkus - quarkus-jdbc-postgresql + io.quarkus + quarkus-jdbc-postgresql - io.quarkus - quarkus-hibernate-orm-panache + io.quarkus + quarkus-hibernate-orm-panache io.quarkus diff --git a/backend/src/main/java/org/acme/domain/Unit.java b/backend/src/main/java/org/acme/domain/Unit.java index eb4cef6..771e660 100644 --- a/backend/src/main/java/org/acme/domain/Unit.java +++ b/backend/src/main/java/org/acme/domain/Unit.java @@ -38,7 +38,6 @@ public class Unit extends PanacheEntity { @PlanningVariable public LocalTime startTime; - // TODO: change unit to be the owner, rather than the student being owner @Transient @JsonIgnoreProperties("units") @ManyToMany(mappedBy = "units", fetch = FetchType.LAZY, cascade = CascadeType.ALL) diff --git a/backend/src/main/java/org/acme/rest/StudentResource.java b/backend/src/main/java/org/acme/rest/StudentResource.java deleted file mode 100644 index 7c75d27..0000000 --- a/backend/src/main/java/org/acme/rest/StudentResource.java +++ /dev/null @@ -1,26 +0,0 @@ -package org.acme.rest; - -import jakarta.transaction.Transactional; -import jakarta.ws.rs.*; -import jakarta.ws.rs.core.MediaType; -import jakarta.ws.rs.core.Response; -import org.acme.domain.Student; - -import java.util.List; - -@Path("/students") -public class StudentResource { - @GET - @Produces(MediaType.APPLICATION_JSON) - public List list() { - return Student.listAll(); - } - - @POST - @Transactional - @Consumes(MediaType.APPLICATION_JSON) - public Response createCampus(Student student) { - student.persist(); - return Response.status(Response.Status.CREATED).entity(student).build(); - } -} diff --git a/backend/src/main/java/org/acme/rest/TimetableResource.java b/backend/src/main/java/org/acme/rest/TimetableResource.java index 7e86739..07ba1c5 100644 --- a/backend/src/main/java/org/acme/rest/TimetableResource.java +++ b/backend/src/main/java/org/acme/rest/TimetableResource.java @@ -82,7 +82,6 @@ public Response timetableUpdate(List updatedUnits) { * @param dbUnits List of all Unit objects in the database * @return The updated Unit, null otherwise */ - @PUT @Transactional @Consumes(MediaType.APPLICATION_JSON) public Unit unitUpdate(Unit updatedUnit, List dbUnits) { @@ -129,8 +128,6 @@ public Room findRoom(Room inputRoom) { return null; } - - @Path("/view") @GET @RolesAllowed({"user"}) @Produces(MediaType.APPLICATION_JSON) @@ -147,6 +144,7 @@ public void findByCampusAndDelete(String campusName) { } } + @Path("/example") @GET @RolesAllowed({"user"}) @Transactional @@ -175,48 +173,31 @@ public Timetable solveExample() throws ExecutionException, InterruptedException var problem = new Timetable("Adelaide", List.of( u1, u2, u3, u4 -// new Unit(5, "5", Duration.ofHours(2), List.of(c, d, e)), -// new Unit(6, "6", Duration.ofHours(2), List.of(f, g, h, i)) ), List.of( DayOfWeek.MONDAY, DayOfWeek.TUESDAY, - DayOfWeek.WEDNESDAY -// DayOfWeek.THURSDAY, -// DayOfWeek.FRIDAY + DayOfWeek.WEDNESDAY, + DayOfWeek.THURSDAY, + DayOfWeek.FRIDAY ), List.of( - LocalTime.of(15, 0) -// LocalTime.of(17, 0) -// LocalTime.of(16,0), -// LocalTime.of(23,0) + LocalTime.of(15, 0), + LocalTime.of(17, 0), + LocalTime.of(16,0), + LocalTime.of(23,0) ), List.of(r1, r2, r3) ); - /* - * During this solving phase, new Unit objects will be created with the - * allotted date and Room assignment. - * - * Currently, the 'old' Unit objects in the 'problem' variable and the - * 'new' Unit objects in the 'solution' variable are stored as different - * Units in the database due to our inability to control the behaviour - * of solverManager.solve - * - * i.e. after solving, there will be 2 copies of each Unit in the - * database, where the 'old' Unit has the list of students but no - * timetable assignment, while the 'new' Unit does not have the list - * of students enrolled, but does have the assigned date and room - */ - findByCampusAndDelete(problem.campusName); Timetable solution = solverManager.solve("job 1", problem).getFinalBestSolution(); - solution.persist(); // saves the solution timetable and all related entities to database + solution.persist(); return solution; } diff --git a/backend/src/main/resources/application.properties b/backend/src/main/resources/application.properties index ea01c7f..5b69cb6 100644 --- a/backend/src/main/resources/application.properties +++ b/backend/src/main/resources/application.properties @@ -32,6 +32,17 @@ quarkus.datasource.jdbc.initial-size=10 quarkus.datasource.jdbc.idle-removal-interval=5M quarkus.datasource.jdbc.max-lifetime=30M +# Swagger UI for endpoint descriptions +quarkus.swagger-ui.always-include=true +quarkus.smallrye-openapi.info-title=Timetabling for VIT +%dev.quarkus.smallrye-openapi.info-title=Timetabling for VIT (development) +%test.quarkus.smallrye-openapi.info-title=Timetabling for VIT (test) +quarkus.smallrye-openapi.info-version=1.0.1 +quarkus.smallrye-openapi.info-description=AI-powered timetabling solution for VIT +quarkus.smallrye-openapi.info-contact-email=techsupport@example.com +quarkus.smallrye-openapi.info-contact-name=JetEdge Customer Support +quarkus.smallrye-openapi.info-license-name=Apache 2.0 +quarkus.smallrye-openapi.info-license-url=https://www.apache.org/licenses/LICENSE-2.0.html # ------------------------------------------- # Debuggers diff --git a/docs/Architecture Diagram.png b/docs/Architecture Diagram.png new file mode 100644 index 0000000..c72ea43 Binary files /dev/null and b/docs/Architecture Diagram.png differ diff --git a/docs/Database ER Diagram.png b/docs/Database ER Diagram.png new file mode 100644 index 0000000..fa196d5 Binary files /dev/null and b/docs/Database ER Diagram.png differ diff --git a/docs/Domain Model.png b/docs/Domain Model.png new file mode 100644 index 0000000..3968c60 Binary files /dev/null and b/docs/Domain Model.png differ diff --git a/docs/Flow Diagram.png b/docs/Flow Diagram.png new file mode 100644 index 0000000..13e021a Binary files /dev/null and b/docs/Flow Diagram.png differ diff --git a/docs/Motivational Model.png b/docs/Motivational Model.png new file mode 100644 index 0000000..7bbc574 Binary files /dev/null and b/docs/Motivational Model.png differ diff --git a/docs/User Stories.xlsx b/docs/User Stories.xlsx new file mode 100644 index 0000000..707e62e Binary files /dev/null and b/docs/User Stories.xlsx differ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f58f72d..5474bbc 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,14 +27,20 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.2", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/file-saver": "^2.0.7", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.1", + "@vitest/ui": "^2.1.3", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", + "jsdom": "^25.0.1", "moment": "^2.30.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", @@ -42,6 +48,13 @@ "vitest": "^2.1.1" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.0.tgz", + "integrity": "sha512-Ff9+ksdQQB3rMncgqDK78uLznstjyfIf2Arnh22pW8kBpLs6rpKDwgnZT46hin5Hl1WzazzK64DOrhSwYpS7bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -1450,6 +1463,13 @@ "node": ">= 8" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "dev": true, + "license": "MIT" + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -1693,6 +1713,252 @@ "win32" ] }, + "node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@testing-library/dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.2.tgz", + "integrity": "sha512-P6GJD4yqc9jZLbe98j/EkyQDTPgqftohZF5FBkHY5BUERZmcf4HeO2k0XaefEg329ux2p21i1A1DmyQ1kKw2Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "chalk": "^3.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "lodash": "^4.17.21", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/jest-dom/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/@testing-library/jest-dom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@testing-library/react": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.0.1.tgz", + "integrity": "sha512-dSmwJVtJXmku+iocRhWOUFbrERC76TX2Mnf0ATODz8brzAZrMBbzLwQixlBSanZxR6LddK3eiwpSFZgDET1URg==", + "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", + "@types/react-dom": "^18.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.5.2", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", + "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "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" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1772,9 +2038,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", - "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "version": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -1782,9 +2048,9 @@ } }, "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2063,14 +2329,14 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.1.tgz", - "integrity": "sha512-YeueunS0HiHiQxk+KEOnq/QMzlUuOzbU1Go+PgAsHvvv3tUkJPm9xWt+6ITNTlzsMXUjmgm5T+U7KBPK2qQV6w==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz", + "integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.1", - "@vitest/utils": "2.1.1", + "@vitest/spy": "2.1.3", + "@vitest/utils": "2.1.3", "chai": "^5.1.1", "tinyrainbow": "^1.2.0" }, @@ -2079,13 +2345,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.1.tgz", - "integrity": "sha512-LNN5VwOEdJqCmJ/2XJBywB11DLlkbY0ooDJW3uRX5cZyYCrc4PI/ePX0iQhE3BiEGiQmK4GE7Q/PqCkkaiPnrA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz", + "integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "^2.1.0-beta.1", + "@vitest/spy": "2.1.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.11" }, @@ -2093,7 +2359,7 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/spy": "2.1.1", + "@vitest/spy": "2.1.3", "msw": "^2.3.5", "vite": "^5.0.0" }, @@ -2107,9 +2373,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.1.tgz", - "integrity": "sha512-SjxPFOtuINDUW8/UkElJYQSFtnWX7tMksSGW0vfjxMneFqxVr8YJ979QpMbDW7g+BIiq88RAGDjf7en6rvLPPQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz", + "integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2120,13 +2386,13 @@ } }, "node_modules/@vitest/runner": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.1.tgz", - "integrity": "sha512-uTPuY6PWOYitIkLPidaY5L3t0JJITdGTSwBtwMjKzo5O6RCOEncz9PUN+0pDidX8kTHYjO0EwUIvhlGpnGpxmA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz", + "integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.1", + "@vitest/utils": "2.1.3", "pathe": "^1.1.2" }, "funding": { @@ -2134,13 +2400,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.1.tgz", - "integrity": "sha512-BnSku1WFy7r4mm96ha2FzN99AZJgpZOWrAhtQfoxjUU5YMRpq1zmHRq7a5K9/NjqonebO7iVDla+VvZS8BOWMw==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz", + "integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.1", + "@vitest/pretty-format": "2.1.3", "magic-string": "^0.30.11", "pathe": "^1.1.2" }, @@ -2149,9 +2415,9 @@ } }, "node_modules/@vitest/spy": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.1.tgz", - "integrity": "sha512-ZM39BnZ9t/xZ/nF4UwRH5il0Sw93QnZXd9NAZGRpIgj0yvVwPpLd702s/Cx955rGaMlyBQkZJ2Ir7qyY48VZ+g==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz", + "integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2161,14 +2427,43 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/ui": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-2.1.3.tgz", + "integrity": "sha512-2XwTrHVJw3t9NYES26LQUYy51ZB8W4bRPgqUH2Eyda3kIuOlYw1ZdPNU22qcVlUVx4WKgECFQOSXuopsczuVjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.1.3", + "fflate": "^0.8.2", + "flatted": "^3.3.1", + "pathe": "^1.1.2", + "sirv": "^2.0.4", + "tinyglobby": "^0.2.6", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "2.1.3" + } + }, + "node_modules/@vitest/ui/node_modules/fflate": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz", + "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==", + "dev": true, + "license": "MIT" + }, "node_modules/@vitest/utils": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.1.tgz", - "integrity": "sha512-Y6Q9TsI+qJ2CC0ZKj6VBb+T8UPz593N113nnUykqwANqhgf3QkZeHFlusgKLTqrnVHbj/XDKZcDHol+dxVT+rQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz", + "integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.1", + "@vitest/pretty-format": "2.1.3", "loupe": "^3.1.1", "tinyrainbow": "^1.2.0" }, @@ -2227,6 +2522,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agent-base": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -2327,6 +2635,16 @@ "dev": true, "license": "Python-2.0" }, + "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", + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -2347,6 +2665,13 @@ "node": ">=12" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/babel-plugin-macros": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", @@ -2713,6 +3038,19 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", @@ -2832,6 +3170,13 @@ "node": ">= 8" } }, + "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/cssfilter": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz", @@ -2839,15 +3184,42 @@ "license": "MIT", "peer": true }, + "node_modules/cssstyle": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.1.0.tgz", + "integrity": "sha512-h66W1URKpBS5YMI/V8PyXvTMFT8SupJ1IzoIV8IeBC/ji8WVmrO8dGlTi+2dh6whmdk6BiKJLD/ZBkhWbcg6nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "rrweb-cssom": "^0.7.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, - "node_modules/debug": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "license": "MIT", "dependencies": { @@ -2862,6 +3234,13 @@ } } }, + "node_modules/decimal.js": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", + "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", + "dev": true, + "license": "MIT" + }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -2888,6 +3267,26 @@ "dev": true, "license": "MIT" }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "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", + "engines": { + "node": ">=6" + } + }, "node_modules/dexie": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.0.8.tgz", @@ -2918,6 +3317,13 @@ "node": ">=8" } }, + "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" + }, "node_modules/dom-helpers": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", @@ -2956,6 +3362,19 @@ "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3362,21 +3781,6 @@ "dev": true, "license": "MIT" }, - "node_modules/fast-url-parser": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/fast-url-parser/-/fast-url-parser-1.1.3.tgz", - "integrity": "sha512-5jOCVXADYNuRkKFzNJ0dCCewsZiYo0dz8QNYljkOpFC6r2U4OBmKtvm/Tsuh4w1YYdDqDb31a8TVhBJ2OJKdqQ==", - "license": "MIT", - "dependencies": { - "punycode": "^1.3.2" - } - }, - "node_modules/fast-url-parser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "license": "MIT" - }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -3469,6 +3873,21 @@ "dev": true, "license": "ISC" }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -3517,16 +3936,6 @@ "node": ">=6.9.0" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -3635,6 +4044,47 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3644,6 +4094,19 @@ "node": ">=10.17.0" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -3686,6 +4149,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -3798,6 +4271,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3853,6 +4333,47 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsdom": { + "version": "25.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-25.0.1.tgz", + "integrity": "sha512-8i7LzZj7BF8uplX+ZyOlIz86V6TAsSs+np6m1kpW9u0JWi4z/1t+FzcK1aek+ybTnAC4KhBL4uXCNT0wcUIeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.1.0", + "data-urls": "^5.0.0", + "decimal.js": "^10.4.3", + "form-data": "^4.0.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.5", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.12", + "parse5": "^7.1.2", + "rrweb-cssom": "^0.7.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^2.11.2" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, "node_modules/jsesc": { "version": "2.5.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", @@ -4005,6 +4526,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -4025,14 +4553,11 @@ } }, "node_modules/loupe": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", - "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.2.tgz", + "integrity": "sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==", "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } + "license": "MIT" }, "node_modules/lru-cache": { "version": "5.1.1", @@ -4044,10 +4569,20 @@ "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", + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { - "version": "0.30.11", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", - "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", "dev": true, "license": "MIT", "dependencies": { @@ -4123,6 +4658,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4153,6 +4698,16 @@ "node": "*" } }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4219,6 +4774,13 @@ "node": ">=8" } }, + "node_modules/nwsapi": { + "version": "2.2.13", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.13.tgz", + "integrity": "sha512-cTGB9ptp9dY9A5VbMSe7fQBcl/tt22Vcqdq8+eN93rblOuE0aCFu4aZ2vMwct/2t+lFnosm8RkQW1I0Omb1UtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -4338,6 +4900,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse5": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.0.tgz", + "integrity": "sha512-ZkDsAOcxsUMZ4Lz5fVciOehNcJ+Gb8gTzcA4yl3wnc273BAybYWrQ+Ks/OjCjSEpjvQkDSeZbybK9qj2VHHdGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -4370,9 +4945,9 @@ "license": "MIT" }, "node_modules/path-to-regexp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-2.2.1.tgz", - "integrity": "sha512-gu9bD6Ta5bwGrrU8muHzVOBFFREpp2iRkVfhBJahwJ6p6Xw20SjT0MxLnwkjOibQmGSYhiUnf2FLe7k+jcFmGQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", + "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", "license": "MIT" }, "node_modules/path-type": { @@ -4459,6 +5034,41 @@ "node": ">= 0.8.0" } }, + "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", + "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/pretty-format/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", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/pretty-format/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" + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -4670,6 +5280,20 @@ "util-deprecate": "~1.0.1" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", @@ -4780,6 +5404,13 @@ "fsevents": "~2.3.2" } }, + "node_modules/rrweb-cssom": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz", + "integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -4810,6 +5441,26 @@ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", "license": "MIT" }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -4830,9 +5481,9 @@ } }, "node_modules/serve": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.3.tgz", - "integrity": "sha512-VqUFMC7K3LDGeGnJM9h56D3XGKb6KGgOw0cVNtA26yYXHCcpxf3xwCTUaQoWlVS7i8Jdh3GjQkOB23qsXyjoyQ==", + "version": "14.2.4", + "resolved": "https://registry.npmjs.org/serve/-/serve-14.2.4.tgz", + "integrity": "sha512-qy1S34PJ/fcY8gjVGszDB3EXiPSk5FKhUa7tQe0UPRddxRidc2V6cNHPNewbE1D7MAkgLuWEt3Vw56vYy73tzQ==", "license": "MIT", "dependencies": { "@zeit/schemas": "2.36.0", @@ -4844,7 +5495,7 @@ "clipboardy": "3.0.0", "compression": "1.7.4", "is-port-reachable": "4.0.0", - "serve-handler": "6.1.5", + "serve-handler": "6.1.6", "update-check": "1.5.4" }, "bin": { @@ -4855,18 +5506,17 @@ } }, "node_modules/serve-handler": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.5.tgz", - "integrity": "sha512-ijPFle6Hwe8zfmBxJdE+5fta53fdIY0lHISJvuikXB3VYFafRjMRpOffSPvCYsbKyBA7pvy9oYr/BT1O3EArlg==", + "version": "6.1.6", + "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", + "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", "license": "MIT", "dependencies": { "bytes": "3.0.0", "content-disposition": "0.5.2", - "fast-url-parser": "1.1.3", "mime-types": "2.1.18", "minimatch": "3.1.2", "path-is-inside": "1.0.2", - "path-to-regexp": "2.2.1", + "path-to-regexp": "3.3.0", "range-parser": "1.2.0" } }, @@ -4965,6 +5615,21 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "license": "ISC" }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5082,6 +5747,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -5125,6 +5803,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -5146,6 +5831,48 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyglobby": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.9.tgz", + "integrity": "sha512-8or1+BGEdk1Zkkw2ii16qSS7uVrQJPre5A9o/XkWPATkk23FZh/15BKFxPnlTy6vkljZxLqYCzzBMj30ZrSvjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.2.tgz", + "integrity": "sha512-KnhMXsKSPZlAhp7+IjUkRZKPb4fUyccpDrdFXbi4QL1qkmFh9kVY09Yox+n4MaOb3lHZ1Tv829C3oaaXoMYPDQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.1.tgz", @@ -5176,6 +5903,26 @@ "node": ">=14.0.0" } }, + "node_modules/tldts": { + "version": "6.1.53", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.53.tgz", + "integrity": "sha512-4uCStuOjPFaY2/LUjTSwdnJTC82W/gvSFL6FoTC9ehNOHboA9cyO3wX1erh2yGofVls37OdXr5sQLEfL5hS1TA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.53" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.53", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.53.tgz", + "integrity": "sha512-IleS872aGdTB/UtocD2dSZBnQi/nqMIZxxezVgfcKKjw6+G2hJGzFw9buIDJO2MVJyEJe3rCAdyMTl2yvGMMrQ==", + "dev": true, + "license": "MIT" + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", @@ -5198,6 +5945,42 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.0.0.tgz", + "integrity": "sha512-FRKsF7cz96xIIeMZ82ehjC3xW2E+O2+v11udrDYewUbszngYhsGa8z6YUMMzO9QJZzzyd0nGGXnML/TReX6W8Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", + "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -5490,9 +6273,9 @@ } }, "node_modules/vite-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.1.tgz", - "integrity": "sha512-N/mGckI1suG/5wQI35XeR9rsMsPqKXzq1CdUndzVstBj/HvyxxGctwnK6WX43NGt5L3Z5tcRf83g4TITKJhPrA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz", + "integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==", "dev": true, "license": "MIT", "dependencies": { @@ -5512,19 +6295,19 @@ } }, "node_modules/vitest": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.1.tgz", - "integrity": "sha512-97We7/VC0e9X5zBVkvt7SGQMGrRtn3KtySFQG5fpaMlS+l62eeXRQO633AYhSTC3z7IMebnPPNjGXVGNRFlxBA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz", + "integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "2.1.1", - "@vitest/mocker": "2.1.1", - "@vitest/pretty-format": "^2.1.1", - "@vitest/runner": "2.1.1", - "@vitest/snapshot": "2.1.1", - "@vitest/spy": "2.1.1", - "@vitest/utils": "2.1.1", + "@vitest/expect": "2.1.3", + "@vitest/mocker": "2.1.3", + "@vitest/pretty-format": "^2.1.3", + "@vitest/runner": "2.1.3", + "@vitest/snapshot": "2.1.3", + "@vitest/spy": "2.1.3", + "@vitest/utils": "2.1.3", "chai": "^5.1.1", "debug": "^4.3.6", "magic-string": "^0.30.11", @@ -5535,7 +6318,7 @@ "tinypool": "^1.0.0", "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "2.1.1", + "vite-node": "2.1.3", "why-is-node-running": "^2.3.0" }, "bin": { @@ -5550,8 +6333,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.1", - "@vitest/ui": "2.1.1", + "@vitest/browser": "2.1.3", + "@vitest/ui": "2.1.3", "happy-dom": "*", "jsdom": "*" }, @@ -5576,6 +6359,66 @@ } } }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-url": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.0.0.tgz", + "integrity": "sha512-1lfMEm2IEr7RIV+f4lUNPOqfFL+pO+Xw3fJSqmjX9AbXcXcYOkCe1P6+9VBZB6n94af16NfZf+sSk0JCBZC9aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.0.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -5689,6 +6532,45 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xss": { "version": "1.0.15", "resolved": "https://registry.npmjs.org/xss/-/xss-1.0.15.tgz", diff --git a/frontend/package.json b/frontend/package.json index bfc6b7a..cc71d62 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,14 +31,20 @@ }, "devDependencies": { "@eslint/js": "^9.9.0", + "@testing-library/dom": "^10.4.0", + "@testing-library/jest-dom": "^6.6.2", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", "@types/file-saver": "^2.0.7", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", + "@types/react": "^18.3.11", + "@types/react-dom": "^18.3.1", "@vitejs/plugin-react": "^4.3.1", + "@vitest/ui": "^2.1.3", "eslint": "^9.9.0", "eslint-plugin-react-hooks": "^5.1.0-rc.0", "eslint-plugin-react-refresh": "^0.4.9", "globals": "^15.9.0", + "jsdom": "^25.0.1", "moment": "^2.30.1", "typescript": "^5.5.3", "typescript-eslint": "^8.0.1", diff --git a/frontend/src/components/GanttChart.tsx b/frontend/src/components/GanttChart.tsx index 6364c4c..276330f 100644 --- a/frontend/src/components/GanttChart.tsx +++ b/frontend/src/components/GanttChart.tsx @@ -1,6 +1,11 @@ import { useEffect, useRef, useState } from "react"; import { Id } from "vis-data/declarations/data-interface"; -import { DataGroupCollectionType, DataItemCollectionType, DataSet, Timeline } from "vis-timeline/standalone"; +import { + DataGroupCollectionType, + DataItemCollectionType, + DataSet, + Timeline, +} from "vis-timeline/standalone"; import "vis-timeline/styles/vis-timeline-graph2d.min.css"; import "../styles/ganttUnassignable.css"; import { useAuthContext } from "../security/AuthContext"; @@ -21,6 +26,14 @@ import JSZip from "jszip"; import { saveAs } from "file-saver"; import { Button } from "@mui/material"; +/** + * Component for displaying a Gantt chart with interactive functionalities. + * Retrieves timetable solutions, initializes the timeline, handles item + * selection, CSV conversion, downloading, and saving data. + * + * Throws errors if timetable solutions are missing in the database or if there + * are issues during data saving. + */ export default function GanttChart() { const params = useParams(); const { authHeader } = useAuthContext(); @@ -53,7 +66,6 @@ export default function GanttChart() { items.current.add(timelineData.activities); useEffect(() => { - if (timelineRef.current) { let prevSelected: Id | null = null; @@ -80,6 +92,12 @@ export default function GanttChart() { options ); + /** + * Checks if an unit overlaps with existing units in the Gantt chart. + * + * @param newItem The new unit to check for overlap + * @returns Boolean indicating if there is an overlap + */ const hasOverlap = (newItem: GanttItem | null) => { const existingItems = items.current.get(); return existingItems.some((item: GanttItem) => { @@ -101,13 +119,22 @@ export default function GanttChart() { }); }; + /** + * Checks if the given unit belongs to a room rather than a building. + * @param item - The unit to be validated. + * @returns True if the item belongs to a room, false otherwise. + */ const validGroup = (item: GanttItem | null) => { if (item == null) return true; - const group: GanttGroup|null = groups.current.get(item.group as Id); - return (group !== null && group.treeLevel === 2); - + const group: GanttGroup | null = groups.current.get(item.group as Id); + return group !== null && group.treeLevel === 2; }; + /** + * Handles the selection event on the timeline, + * checks for overlaps and valid room assignments, + * and updates the list of modified units accordingly. + */ timeline.on("select", (properties) => { if (prevSelected !== null) { const overlaps = hasOverlap(items.current.get(prevSelected)); @@ -119,27 +146,35 @@ export default function GanttChart() { alert("ASSIGN ACTIVITIES TO ROOMS ONLY"); } if (!overlaps && inRoom) { - let rawStartDate: rawDate = toRawDate(items.current.get(prevSelected)?.start as Date); + let rawStartDate: rawDate = toRawDate( + items.current.get(prevSelected)?.start as Date + ); let rawEndDate: rawDate = toRawDate( items.current.get(prevSelected)?.end as Date ); - let unitId:number|undefined = items.current.get(prevSelected)?.UnitId; - let campus: string|undefined = items.current.get(prevSelected)?.campus; - let buildingId: string|undefined = groups.current.get( - groups.current.get( - items.current.get(prevSelected)?.group as Id - )?.parent as Id - )?.originalId - let roomCode: string|undefined = groups.current.get( - items.current.get(prevSelected)?.group as Id - )?.originalId; - if ( unitId !== undefined && campus !== undefined && buildingId !== undefined && roomCode !== undefined) { + let unitId: number | undefined = + items.current.get(prevSelected)?.UnitId; + let campus: string | undefined = + items.current.get(prevSelected)?.campus; + let buildingId: string | undefined = groups.current.get( + groups.current.get(items.current.get(prevSelected)?.group as Id) + ?.parent as Id + )?.originalId; + let roomCode: string | undefined = groups.current.get( + items.current.get(prevSelected)?.group as Id + )?.originalId; + if ( + unitId !== undefined && + campus !== undefined && + buildingId !== undefined && + roomCode !== undefined + ) { const modded: Unit = { campus: "", - name:"", + name: "", course: "", - duration:-1, - students:[], + duration: -1, + students: [], wantsLab: false, unitId: unitId, room: { @@ -177,7 +212,13 @@ export default function GanttChart() { } }, []); - const convertToCSV = (course:string) => { + /** + * Converts the items related to a specific course into CSV format. + * + * @param course - The course for which items need to be converted to CSV. + * @returns A string representing the items related to the course in CSV format. + */ + const convertToCSV = (course: string) => { let csvContent = "id,name,start,end,room,building\n"; const itemList = items.current.get(); itemList.forEach((item) => { @@ -187,10 +228,8 @@ export default function GanttChart() { csvContent += `${item.id},${item.content},${start},${end},${ groups.current.get(item.group as Id)?.content }, ${ - groups.current.get( - groups.current.get(item.group as Id) - ?.parent as Id - )?.originalId + groups.current.get(groups.current.get(item.group as Id)?.parent as Id) + ?.originalId }\n`; } }); @@ -198,6 +237,10 @@ export default function GanttChart() { return csvContent; }; + /** + * Downloads a zip file containing CSV files for each unique course in the + * items list. + */ const downloadCSV = () => { const zip = new JSZip(); const uniqueNames = new Set(items.current.map((obj) => obj.course)); @@ -212,7 +255,7 @@ export default function GanttChart() { csvData.replace(/\u00A0/g, " ") ); } - }) + }); zip.generateAsync({ type: "blob" }).then((content) => { saveAs( @@ -222,13 +265,20 @@ export default function GanttChart() { }); }; + /** + * Asynchronously saves data to the remote API backend. + * Opens a loading modal during the save operation. + * Handles PUT and GET requests to update and view timetable data. + * Parses the JSON response and stores timetable solutions in sessionStorage. + * Catches and logs any errors that occur during the save operation. + */ const saveData = async () => { try { setOpen(true); const response = await fetch(REMOTE_API_URL + "/timetabling/update", { method: "PUT", headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", }, body: JSON.stringify(moddedUnits.current), }); @@ -236,19 +286,17 @@ export default function GanttChart() { if (!response.ok) { throw new Error("Failed to save data, error in GanttChart.tsx"); } - // Clear moddedUnits if the first fetch was successful + moddedUnits.current = []; // Second fetch request (GET) only runs after the first one completes - const viewResponse = await fetch(REMOTE_API_URL + "/timetabling/view", { + const viewResponse = await fetch(REMOTE_API_URL + "/timetabling", { headers: { Authorization: authHeader }, }); if (!viewResponse.ok) { throw new Error("Network response was not ok"); } - - // Parse the JSON response and store in sessionStorage const data = await viewResponse.json(); const timetableSolutions: TimetableSolution[] = data as TimetableSolution[]; @@ -264,10 +312,13 @@ export default function GanttChart() { return (
-
+
diff --git a/frontend/src/pages/LoginPage.tsx b/frontend/src/pages/LoginPage.tsx index 195e094..225395f 100644 --- a/frontend/src/pages/LoginPage.tsx +++ b/frontend/src/pages/LoginPage.tsx @@ -6,6 +6,13 @@ import VIT_Logo from '../assets/logo.png'; import { REMOTE_API_URL } from '../scripts/api'; import LoadingButton from '../components/LoadingButton'; +/** + * Component for rendering the login page with username and password input + * fields. + * Handles form submission to validate credentials with the backend. + * If credentials are valid, sets the authorization header and navigates to + * the enrolment page. + */ export default function LoginPage() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); diff --git a/frontend/src/pages/SendData.tsx b/frontend/src/pages/SendData.tsx index e96a357..6f35776 100644 --- a/frontend/src/pages/SendData.tsx +++ b/frontend/src/pages/SendData.tsx @@ -16,7 +16,6 @@ import LoadingButton from "../components/LoadingButton"; * (will remove and replace with display page in next sprint). * * @returns button for sending timetable problem and temporary display for timetable solution. - * TODO: change button and UI elements to fit with VIT themes. */ export default function SendData() { diff --git a/frontend/src/pages/TimetableMod.tsx b/frontend/src/pages/TimetableMod.tsx index 45d8544..d17b07a 100644 --- a/frontend/src/pages/TimetableMod.tsx +++ b/frontend/src/pages/TimetableMod.tsx @@ -20,7 +20,7 @@ export default function TimetableMod() { const { authHeader } = useAuthContext(); useEffect(() => { - fetch(REMOTE_API_URL + "/timetabling/view", { headers: { 'Authorization': authHeader } }) + fetch(REMOTE_API_URL + "/timetabling", { headers: { 'Authorization': authHeader } }) .then((response) => { if (!response.ok) { throw new Error("Network response was not ok"); diff --git a/frontend/src/scripts/solutionParsing.ts b/frontend/src/scripts/solutionParsing.ts index 66bf109..1283b3a 100644 --- a/frontend/src/scripts/solutionParsing.ts +++ b/frontend/src/scripts/solutionParsing.ts @@ -1,14 +1,11 @@ import { TimetableSolution, Weekday } from "./api"; -import { - TimelineGroup, - TimelineItem, -} from "vis-timeline/standalone"; +import { TimelineGroup, TimelineItem } from "vis-timeline/standalone"; export type GanttItem = TimelineItem & { UnitId: number; campus: string; course: string; -} +}; export type GanttGroup = TimelineGroup & { treeLevel: number; @@ -23,11 +20,19 @@ export type GanttItems = { }; export type rawDate = { - dayOfWeek: Weekday, - time: string, + dayOfWeek: Weekday; + time: string; }; const startDate = "2024-10-14"; +/** + * Format the given backend solution into suitable format for displaying in the + * Ganttchart. + * + * @param campusSolution The timetable solution for a specific campus. + * @returns GanttItems containing activities, rooms, and buildings for + * visualization. + */ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { let ganttActivities: GanttItem[] = []; let ganttRooms: GanttGroup[] = []; @@ -38,31 +43,27 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { const groupEnum = new Map(); let counter = 1; - campusSolution.units.forEach((activity) => { - // console.log("start"); if (!activityEnum.has(activity.unitId)) { activityEnum.set(activity.unitId, counter); counter++; } - let newRoom:boolean; - let newBuilding:boolean; + let newRoom: boolean; + let newBuilding: boolean; newRoom = false; newBuilding = false; if (!groupEnum.has(activity.room.roomCode)) { groupEnum.set(activity.room.roomCode, counter); counter++; newRoom = true; - } if (!groupEnum.has(activity.room.buildingId)) { groupEnum.set(activity.room.buildingId, counter); counter++; newBuilding = true; } - // console.log(buildingLookup.get(activity.room.buildingId)?.nestedGroups); //=============================Handle Rooms================================= - if (newRoom) { + if (newRoom) { const ganttRoom: GanttGroup = { originalId: activity.room.roomCode, id: groupEnum.get(activity.room.roomCode) || 0, @@ -87,7 +88,7 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { //=============================Handle Buildings============================= if (newBuilding) { const ganttBuilding: GanttGroup = { - originalId:activity.room.buildingId, + originalId: activity.room.buildingId, id: groupEnum.get(activity.room.buildingId) || 0, content: activity.room.buildingId, treeLevel: 1, @@ -114,7 +115,6 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { const buildingCheck = buildingLookup.get(activity.room.buildingId); const roomGroup = groupEnum.get(activity.room.roomCode); - // console.log(roomGroup); if (buildingCheck && roomGroup) { if ( buildingCheck.nestedGroups !== undefined && @@ -125,7 +125,6 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { } else { throw new Error("LOGIC ERROR IN getGanttItems"); } - // console.log("end"); }); _return = { @@ -136,8 +135,29 @@ export function getGanttItems(campusSolution: TimetableSolution): GanttItems { return _return; } -function parseDate(startDate: string, dayOfWeek: string, startTime: string):Date { - const daysOfWeek = ['SUNDAY', 'MONDAY', 'TUESDAY', 'WEDNESDAY', 'THURSDAY', 'FRIDAY', 'SATURDAY']; +/** + * Parses the output backend date format into suitable frontend format for + * displaying + * + * @param startDate The starting date in string format (e.g., '2024-10-14') + * @param dayOfWeek The day of the week in string format (e.g., 'MONDAY'). + * @param startTime The starting time in string format (e.g., 'HH:MM:SS'). + * @returns The calculated final date based on the input parameters. + */ +function parseDate( + startDate: string, + dayOfWeek: string, + startTime: string +): Date { + const daysOfWeek = [ + "SUNDAY", + "MONDAY", + "TUESDAY", + "WEDNESDAY", + "THURSDAY", + "FRIDAY", + "SATURDAY", + ]; const baseDate = new Date(startDate); @@ -146,14 +166,21 @@ function parseDate(startDate: string, dayOfWeek: string, startTime: string):Date const dayDifference = (targetDayIndex + 7 - currentDayIndex) % 7; - const [hours, minutes, seconds] = startTime.split(':').map(Number); + const [hours, minutes, seconds] = startTime.split(":").map(Number); const finalDate = new Date(baseDate); finalDate.setDate(baseDate.getDate() + dayDifference); - finalDate.setHours(hours, minutes, seconds, 0); + finalDate.setHours(hours, minutes, seconds, 0); return finalDate; } +/** + * Converts a JavaScript Date object to the correct format for the backend + * framework. + * @param date The Date object to convert. + * @returns The rawDate object with the day of the week (as Weekday enum) and + * the time in "HH:MM:SS" format. + */ export function toRawDate(date: Date): rawDate { const daysOfWeek = [ "SUNDAY", @@ -178,16 +205,13 @@ export function toRawDate(date: Date): rawDate { return { dayOfWeek: dayOfWeek as Weekday, time: startTime }; } -//TODO: Parse data to send to backend -export function formatSolution2Save(items: GanttItems) { - items -} - -//TODO: Parse data for downloading -export function format2CSV(items: GanttItems) { - items -} - +/** + * Finds a specific campus solution from a list of timetable solutions. + * @param campus - The name of the campus to search for. + * @param solutions - An array of timetable solutions to search within. + * @returns The timetable solution corresponding to the specified campus, + * or null if not found. + */ export function findCampusSolution( campus: string, solutions: TimetableSolution[] diff --git a/frontend/src/tests/api.test.ts b/frontend/src/tests/api.test.ts index 0f26572..981864e 100644 --- a/frontend/src/tests/api.test.ts +++ b/frontend/src/tests/api.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { fetchTimetableSolution, LOCAL_API_URL, TimetableProblem } from '../scripts/api'; +import { fetchTimetableSolution, REMOTE_API_URL, TimetableProblem } from '../scripts/api'; import moment from 'moment'; import { AuthHeader } from '../security/AuthContext'; @@ -23,7 +23,7 @@ describe('fetchTimetableSolution', { timeout: 200000 }, () => { const authHeader: AuthHeader = `Basic ${btoa(`${import.meta.env.VITE_FRONTEND_USERNAME}:${import.meta.env.VITE_FRONTEND_PASSWORD}`)}`; - const solution = await fetchTimetableSolution(problem, authHeader, LOCAL_API_URL); + const solution = await fetchTimetableSolution(problem, authHeader, REMOTE_API_URL); expect(solution).not.toBeNull(); expect(solution?.units[0].dayOfWeek).toEqual(problem.daysOfWeek[0]); expect(solution?.units[0].startTime).toEqual(problem.startTimes[0]); @@ -55,7 +55,7 @@ describe('fetchTimetableSolution', { timeout: 200000 }, () => { }; const authHeader: AuthHeader = `Basic ${btoa(`${import.meta.env.VITE_FRONTEND_USERNAME}:${import.meta.env.VITE_FRONTEND_PASSWORD}`)}`; - const solutions = await Promise.all([fetchTimetableSolution(problem0, authHeader, LOCAL_API_URL), fetchTimetableSolution(problem1, authHeader, LOCAL_API_URL)]); + const solutions = await Promise.all([fetchTimetableSolution(problem0, authHeader, REMOTE_API_URL), fetchTimetableSolution(problem1, authHeader, REMOTE_API_URL)]); for (let i = 0; i < solutions.length; i++) { expect(solutions[i]).not.toBeNull(); diff --git a/frontend/src/tests/components/BackButton.test.tsx b/frontend/src/tests/components/BackButton.test.tsx new file mode 100644 index 0000000..a8ce94c --- /dev/null +++ b/frontend/src/tests/components/BackButton.test.tsx @@ -0,0 +1,38 @@ +import { afterEach, describe, it, expect } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import BackButton from '../../components/BackButton'; + +// Automatically clean up the DOM after each test +afterEach(() => { + cleanup(); +}); + +describe('BackButton', () => { + it('renders as an active button by default', () => { + // Render the default BackButton + render(); + + const backButton = screen.getByRole('button'); + + // Check if the button is in the document + expect(backButton).toBeInTheDocument(); + + // Check if the button is enabled (active by default) + expect(backButton).toBeEnabled(); + }); + + it('renders as a disabled button with props', () => { + // Render the BackButton with the disabled prop set to true + render(); + + const backButton = screen.getByRole('button'); + + // Check if the button is in the document + expect(backButton).toBeInTheDocument(); + + // Check if the button is disabled + expect(backButton).toBeDisabled(); + }); + +}); diff --git a/frontend/src/tests/components/DisplayFile.test.tsx b/frontend/src/tests/components/DisplayFile.test.tsx new file mode 100644 index 0000000..c896477 --- /dev/null +++ b/frontend/src/tests/components/DisplayFile.test.tsx @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; +import DisplayFile from '../../components/DisplayFile'; + +describe('DisplayFile', () => { + + it('renders an empty Box when fileChosen is null', () => { + const { container } = render(); + + // The Box is rendered, but nothing else + const emptyBox = container.querySelector('.MuiBox-root'); + expect(emptyBox).toBeInTheDocument(); + expect(emptyBox).toBeEmptyDOMElement(); // Box should be empty when fileChosen is null + }); + + it('renders the file name and FilePresentIcon when fileChosen is not null', () => { + const mockFile = new File([''], 'example.txt', { type: 'text/plain' }); + + render(); + + // Check that the file name is rendered + const fileName = screen.getByText('example.txt'); + expect(fileName).toBeInTheDocument(); + + // Check that the FilePresentIcon is rendered + const fileIcon = screen.getByTestId('FilePresentIcon'); + expect(fileIcon).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/tests/components/LoadingButton.test.tsx b/frontend/src/tests/components/LoadingButton.test.tsx new file mode 100644 index 0000000..2f0e3fc --- /dev/null +++ b/frontend/src/tests/components/LoadingButton.test.tsx @@ -0,0 +1,76 @@ +import { afterEach, describe, it, expect, vi } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import LoadingButton from '../../components/LoadingButton'; +import '@testing-library/jest-dom/vitest'; +import userEvent from '@testing-library/user-event'; + +// Automatically clean up the DOM after each test +afterEach(() => { + cleanup(); +}); + +describe('LoadingButton', () => { + + it('shows a disabled button with CircularProgress and no text when loading is true', () => { + render( + {}} + text="Submit" + /> + ); + + // Check that the button is disabled + const button = screen.getByRole('button'); + expect(button).toBeDisabled(); + + // Check that CircularProgress is present + const circularProgress = screen.getByRole('progressbar'); + expect(circularProgress).toBeInTheDocument(); + + // Check that the button does not display the text "Submit" + expect(button).not.toHaveTextContent('Submit'); + }); + + it('shows an enabled button with text and no CircularProgress when loading is false', () => { + render( + {}} + text="Submit" + /> + ); + + // Check that the button is enabled + const button = screen.getByRole('button'); + expect(button).toBeEnabled(); + + // Check that CircularProgress is not present + const circularProgress = screen.queryByRole('progressbar'); + expect(circularProgress).not.toBeInTheDocument(); + + // Check that the button displays the text "Submit" + expect(button).toHaveTextContent('Submit'); + }); + + it('triggers the onClick function when clicked and not loading', async () => { + const handleClick = vi.fn(); + + render( + + ); + + // Simulate user clicking the button + const button = screen.getByRole('button'); + const user = userEvent.setup(); + await user.click(button); + + // Ensure the onClick function is called + expect(handleClick).toHaveBeenCalledTimes(1); + }); + +}); diff --git a/frontend/src/tests/components/LoadingModal.test.tsx b/frontend/src/tests/components/LoadingModal.test.tsx new file mode 100644 index 0000000..87e238e --- /dev/null +++ b/frontend/src/tests/components/LoadingModal.test.tsx @@ -0,0 +1,34 @@ +import { afterEach, describe, it, expect } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; // For jest-dom matchers +import LoadingModal from '../../components/LoadingModel'; + +// Automatically clean up the DOM after each test +afterEach(() => { + cleanup(); +}); + +describe('LoadingModal', () => { + it('renders the modal with CircularProgress when open is true', () => { + render(); + + // Modal should be present + const modal = screen.getByRole('presentation'); + expect(modal).toBeInTheDocument(); + + // CircularProgress should be present inside the modal + const progress = screen.getByRole('progressbar'); + expect(progress).toBeInTheDocument(); + }); + + it('does not render the modal or CircularProgress when open is false', () => { + render(); + + // Modal and CircularProgress should not be present + const modal = screen.queryByRole('presentation'); + expect(modal).not.toBeInTheDocument(); + + const progress = screen.queryByRole('progressbar'); + expect(progress).not.toBeInTheDocument(); + }); +}); diff --git a/frontend/src/tests/components/ProceedButton.test.tsx b/frontend/src/tests/components/ProceedButton.test.tsx new file mode 100644 index 0000000..f1903d1 --- /dev/null +++ b/frontend/src/tests/components/ProceedButton.test.tsx @@ -0,0 +1,60 @@ +import { afterEach, describe, it, expect, vi } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import ProceedButton from '../../components/ProceedButton'; +import { MemoryRouter, useNavigate } from 'react-router-dom'; +import '@testing-library/jest-dom/vitest'; +import userEvent from '@testing-library/user-event'; + +// Automatically clean up the DOM after each test +afterEach(() => { + cleanup(); +}); + +// Mock useNavigate to test navigation behavior +vi.mock(import('react-router-dom'), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useNavigate: vi.fn(), // Mock useNavigate only + }; +}); + +afterEach(() => { + vi.clearAllMocks(); // Clear mocks after each test +}); + +describe('ProceedButton', () => { + it('renders a disabled button when fileChosen is null', () => { + render( + + + + ); + + // Check that the button is disabled + const button = screen.getByRole('button', { name: /proceed/i }); + expect(button).toBeDisabled(); + }); + + it('renders an enabled button and triggers navigation when fileChosen is provided', async () => { + const navigateMock = vi.fn(); + vi.mocked(useNavigate).mockReturnValue(navigateMock); + const user = userEvent.setup(); + + render( + + + + ); + + // Check that the button is enabled + const button = screen.getByRole('button', { name: /proceed/i }); + expect(button).toBeEnabled(); + + // Simulate clicking the button + await user.click(button); + + // Ensure that navigate is called with the correct route + expect(navigateMock).toHaveBeenCalledWith('/seminfo/room'); + }); +}); diff --git a/frontend/src/tests/components/SkipButton.test.tsx b/frontend/src/tests/components/SkipButton.test.tsx new file mode 100644 index 0000000..cd1b7b0 --- /dev/null +++ b/frontend/src/tests/components/SkipButton.test.tsx @@ -0,0 +1,63 @@ +import { afterEach, describe, it, expect, vi } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import SkipButton from '../../components/SkipButton'; +import { MemoryRouter, useNavigate } from 'react-router-dom'; +import '@testing-library/jest-dom/vitest'; +import userEvent from '@testing-library/user-event'; + +// Automatically clean up the DOM after each test +afterEach(() => { + cleanup(); +}); + +// Mock useNavigate to test navigation behavior +vi.mock(import('react-router-dom'), async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useNavigate: vi.fn(), + }; +}); + +describe('SkipButton', () => { + afterEach(() => { + vi.clearAllMocks(); // Clear mocks after each test + sessionStorage.clear(); // Clear sessionStorage after each test + }); + + it('renders a disabled button when there are no solution in sessionStorage', () => { + render( + + + + ); + + // Check that the button is disabled + const button = screen.getByRole('button', { name: /modify timetable/i }); + expect(button).toBeDisabled(); + }); + + it('renders an enabled button and triggers navigation when solution exists in sessionStorage', async () => { + // Set sessionStorage value + sessionStorage.setItem('campusSolutions', 'true'); + const navigateMock = vi.fn(); + vi.mocked(useNavigate).mockReturnValue(navigateMock); + const user = userEvent.setup(); + + render( + + + + ); + + // Check that the button is enabled + const button = screen.getByRole('button', { name: /modify timetable/i }); + expect(button).toBeEnabled(); + + // Simulate clicking the button + await user.click(button); + + // Ensure that navigate is called with the correct route + expect(navigateMock).toHaveBeenCalledWith('/timetablemod'); + }); +}); diff --git a/frontend/src/tests/components/UploadPopUp.test.tsx b/frontend/src/tests/components/UploadPopUp.test.tsx new file mode 100644 index 0000000..e36061a --- /dev/null +++ b/frontend/src/tests/components/UploadPopUp.test.tsx @@ -0,0 +1,119 @@ +import { afterEach, describe, it, expect, vi, afterAll, beforeAll } from 'vitest'; +import { render, screen, cleanup } from '@testing-library/react'; +import UploadPopUp from '../../components/UploadPopUp'; +import { MemoryRouter } from 'react-router-dom'; +import '@testing-library/jest-dom/vitest'; +import userEvent from '@testing-library/user-event'; + +// Automatically clean up the DOM after each test +afterEach(() => { + cleanup(); +}); + +describe('UploadPopUp', () => { + + beforeAll(() => { + globalThis.alert = vi.fn(); // Mock alert + }); + + afterAll(() => { + vi.restoreAllMocks(); // Restore original alert after tests + }); + + + it('renders the "Generate Timetable" button', () => { + render( + + + + ); + + // Check if the button to open the modal is rendered + const generateButton = screen.getByRole('button', { name: /generate timetable/i }); + expect(generateButton).toBeInTheDocument(); + }); + + it('opens the modal when "Generate Timetable" button is clicked', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + // Click the "Generate Timetable" button + const generateButton = screen.getByRole('button', { name: /generate timetable/i }); + await user.click(generateButton); + + // Check if the modal elements are visible + expect(screen.getByText(/upload file/i)).toBeInTheDocument(); + expect(screen.getByRole('button', { name: /proceed/i })).toBeInTheDocument(); + }); + + it('closes the modal when clicking outside', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + // Open the modal + const generateButton = screen.getByRole('button', { name: /generate timetable/i }); + await user.click(generateButton); + + // Click outside the modal (backdrop) + const backdrop = screen.getByText(/upload file/i).closest('.base-Modal-root')?.querySelector('.base-Backdrop-open'); + if (backdrop) + { + await user.click(backdrop); + } + + // Ensure modal elements are no longer in the DOM + expect(screen.queryByText(/upload file/i)).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /proceed/i })).not.toBeInTheDocument(); + }); + + it('renders disabled ProceedButton when no file is selected', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + // Open the modal + const generateButton = screen.getByRole('button', { name: /generate timetable/i }); + await user.click(generateButton); + + // Ensure Proceed button is disabled initially + const proceedButton = screen.getByRole('button', { name: /proceed/i }); + expect(proceedButton).toBeDisabled(); + }); + + it('does not enable ProceedButton when an invalid file is selected', async () => { + const user = userEvent.setup(); + + render( + + + + ); + + // Open the modal + const generateButton = screen.getByRole('button', { name: /generate timetable/i }); + await user.click(generateButton); + + // Simulate file selection + const file = new File(['file content'], 'example.txt', { type: 'text/plain' }); + const uploadButton = screen.getByText(/upload file/i); // Assuming the text is in UploadButton + await user.upload(uploadButton, file); + + // Check that Proceed button is now enabled + const proceedButton = screen.getByRole('button', { name: /proceed/i }); + expect(proceedButton).not.toBeEnabled(); + }); +}); diff --git a/frontend/src/tests/security.test.ts b/frontend/src/tests/security.test.ts new file mode 100644 index 0000000..584b710 --- /dev/null +++ b/frontend/src/tests/security.test.ts @@ -0,0 +1,42 @@ +import { describe, it, expect } from 'vitest'; +import { REMOTE_API_URL } from '../scripts/api'; +import { AuthHeader } from '../security/AuthContext'; + + +/** + * Test Basic Authentication with backend. + */ +describe('authentication', { timeout: 200000 }, () => { + + it('allow access given correct username and password', async () => { + + const authHeader: AuthHeader = `Basic ${btoa(`${import.meta.env.VITE_FRONTEND_USERNAME}:${import.meta.env.VITE_FRONTEND_PASSWORD}`)}`; + const response = await fetch(REMOTE_API_URL + "/login", { + method: 'GET', + headers: { + 'Authorization': authHeader, + }, + }); + + expect(response.status).not.equal(401); + + const text = await response.text(); + expect(text).equal("jetedge"); + + }); + + it('does not allow access given wrong username or password', async () => { + + const authHeader: AuthHeader = ''; + const response = await fetch(REMOTE_API_URL + "/login", { + method: 'GET', + headers: { + 'Authorization': authHeader, + }, + }); + + expect(response.status).equal(401); + + }); + +}); \ No newline at end of file diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..16950e3 --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react' + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + } +}); \ No newline at end of file diff --git a/tests/Integration Testing.pdf b/tests/Integration Testing.pdf new file mode 100644 index 0000000..00b04a0 Binary files /dev/null and b/tests/Integration Testing.pdf differ diff --git a/tests/Security Testing.pdf b/tests/Security Testing.pdf new file mode 100644 index 0000000..4657a12 Binary files /dev/null and b/tests/Security Testing.pdf differ diff --git a/tests/System Testing.pdf b/tests/System Testing.pdf new file mode 100644 index 0000000..87a43e9 Binary files /dev/null and b/tests/System Testing.pdf differ diff --git a/tests/Unit Testing (Backend).pdf b/tests/Unit Testing (Backend).pdf new file mode 100644 index 0000000..68d6e16 Binary files /dev/null and b/tests/Unit Testing (Backend).pdf differ diff --git a/tests/Unit Testing (Frontend).pdf b/tests/Unit Testing (Frontend).pdf new file mode 100644 index 0000000..c6b2a11 Binary files /dev/null and b/tests/Unit Testing (Frontend).pdf differ diff --git a/tests/User Acceptance Testing.pdf b/tests/User Acceptance Testing.pdf new file mode 100644 index 0000000..4435f8e Binary files /dev/null and b/tests/User Acceptance Testing.pdf differ