diff --git a/.document b/.document new file mode 100644 index 00000000..bb77f18a --- /dev/null +++ b/.document @@ -0,0 +1 @@ ++ app/controllers/experiment_controller.rb \ No newline at end of file diff --git a/.gitignore b/.gitignore index 288c5a2d..374ab4f4 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,5 @@ !/app/assets/builds/.keep /node_modules +/coverage +/doc \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 00000000..c99d2e73 --- /dev/null +++ b/.rspec @@ -0,0 +1 @@ +--require spec_helper diff --git a/Gemfile b/Gemfile index b4f7b825..3a4f9c50 100644 --- a/Gemfile +++ b/Gemfile @@ -45,6 +45,8 @@ gem "bootsnap", require: false # gem "image_processing", "~> 1.2" group :development, :test do + gem 'simplecov', require: false, group: :test + gem 'rspec-rails' # See https://guides.rubyonrails.org/debugging_rails_applications.html#debugging-with-the-debug-gem gem "debug", platforms: %i[ mri mswin mswin64 mingw x64_mingw ] end diff --git a/Gemfile.lock b/Gemfile.lock index f5dbc2bb..67f5c029 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -243,6 +243,23 @@ GEM reline (0.4.0) io-console (~> 0.5) rexml (3.2.6) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-mocks (3.12.6) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.12.0) + rspec-rails (6.1.0) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.12) + rspec-expectations (~> 3.12) + rspec-mocks (~> 3.12) + rspec-support (~> 3.12) + rspec-support (3.12.1) ruby2_keywords (0.0.5) rubyzip (2.3.2) selenium-webdriver (4.9.0) @@ -305,6 +322,7 @@ DEPENDENCIES jsbundling-rails puma (>= 5.0) rails (~> 7.1.1) + rspec-rails selenium-webdriver sprockets-rails sqlite3 (~> 1.4) diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index 288b9ab7..09961503 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -13,3 +13,425 @@ *= require_tree . *= require_self */ + +/* Reset margins and paddings for specific elements */ + +h1, +h2, +h3, +h4, +h5, +h6, +p, +a, +ul, +ol, +li { + margin: 0; + padding: 0; +} + +:root { + --background-color: #edf2f4; + --form-background-color: #8d99ae; + --trial-background-color: #8d99ae; + --class-exp-background-color: #8d99ae; + --class-exp-tag-background-color: #edf2f4; +} + +* { + + font-family: "Roboto", sans-serif; +} + +/* Classificate Experiment Component */ +.classification-experiment { + color: #2b2d42; + display: flex; + flex-direction: column; + gap: 10px; + padding: 10px; + border-radius: 10px; + background-color: var(--class-exp-background-color); + max-width: 300px; + width: 100%; + max-height: 600px; + box-sizing: border-box; +} + +.classification-experiment > h1 { + margin-bottom: 10px; +} + +.classification-experiment > input { + padding: 10px; + border: 1px solid #ddd; + border-radius: 8px; + outline: none; + box-sizing: border-box; + transition: border-color 0.3s ease; + font-size: 10px; +} + +.classification-experiment > input:focus { + border-color: #3498db; +} + +.classification-experiment > ul { + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; + max-height: 100%; + overflow-y: auto; +} + +.classification-experiment > ul > li { + background-color: var(--class-exp-tag-background-color); + padding: 10px; + border-radius: 10px; + margin-right: 10px; +} + +.class-exp-tags ul { + list-style: none; + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + max-height: 100%; + overflow-y: auto; +} + +.class-exp-tags ul > li { + background-color: var(--class-exp-tag-background-color); + padding: 5px; + border-radius: 10px; + font-size: 10px; + cursor: pointer; +} + +.experiment-tags-header { + display: flex; + justify-content: space-between; + align-items: center; + background-color: #e0e1dd; + padding: 6px; + border-radius: 10px; + margin-bottom: 10px; +} + +.experiment-tags-header > button { + font-size: 15px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; +} + +.experiment-tags { + list-style: none; + display: flex; + flex-direction: column; + gap: 10px; +} + +.experiment-tags li { + display: flex; + justify-content: space-between; + align-items: center; + padding: 6px; + background-color: #d6ccc2; + border-radius: 10px; +} + +.experiment-tags li > span { + font-size: 10px; + background-color: #edede9; + padding: 5px; + border-radius: 10px; +} + +.experiment-tags button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; +} + +/* Create Experiment Component */ + +.info { + color: #2b2d42; + font-size: 10px; +} + +.outer-container { + color: #2b2d42; + display: flex; + background-color: var(--background-color); + flex-wrap: wrap; + gap: 20px; + padding: 10px; + border-radius: 10px; +} + +.form-container > h1, .trial-container > h1 { + margin-bottom: 10px; +} + +.form-container { + background-color: var(--form-background-color); + padding: 10px; + border-radius: 10px; + max-width: 300px; + width: 100%; +} + +.form-container > form { + display: flex; + flex-direction: column; + gap: 10px; +} + +.form-container > form hr { + margin: 10px 0; +} + +.form-container > form input { + font-size: 10px; /* Tamanho da fonte do input */ + padding: 10px; /* Espaçamento interno */ + border: 1px solid #ddd; /* Cor da borda */ + border-radius: 8px; /* Cantos arredondados */ + outline: none; /* Remover a borda de foco padrão */ + box-sizing: border-box; /* Incluir padding e border na largura e altura */ + transition: border-color 0.3s ease; /* Adiciona uma transição suave para a cor da borda */ +} + +.form-container > form input:focus { + border-color: #3498db; /* Cor da borda quando em foco */ +} + +.form-container > form button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #edf2f4; + color: #000; +} + +.form-container > form button:hover { + background-color: #2980b9; /* Nova cor de fundo ao passar o mouse */ +} + +.trial-container { + background-color: var(--trial-background-color); + padding: 10px; + border-radius: 10px; + max-width: 300px; + width: 100%; +} + +.trial { + background-color: #edf2f4; + margin: 10px 0; + border-radius: 10px; + padding: 10px; + display: flex; + flex-direction: column; + gap: 10px; +} + +.trial-header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.trial-header > div { + display: flex; + flex-direction: row; + gap: 10px; +} + +.trial-header button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; +} + +.factor-trial { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.factor-trial button { + font-size: 9px; + padding: 5px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; +} + +/* MODAL STYLES */ +.modal { + display: none; /* Hidden by default */ + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + padding-top: 100px; /* Location of the box */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + background-color: rgb(0, 0, 0); /* Fallback color */ + background-color: rgba(0, 0, 0, 0.4); /* Black w/ opacity */ +} + +/* Modal Content */ +/* Default styles */ +.modal-content { + background-color: #fefefe; + margin: auto; + padding: 20px; + border: 1px solid #888; + width: 600px; + height: 600px; + border-radius: 10px; +} + +/* Responsive styles */ +@media (max-width: 768px) { + .modal { + padding: 3px; + } + + .modal-content { + width: 90%; /* Adjust the width for smaller screens */ + height: 90%; /* Allow height to adjust based on content */ + } +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + padding-bottom: 10px; + margin-bottom: 10px; + border-bottom: 1px solid #ddd; +} + +.modal-body { + overflow-y: scroll; + max-height: 90%; + width: 100%; + scroll-behavior: smooth; +} + +.experiment-info { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; +} + +.experiment-info > span { + font-size: 20px; + margin: 3px; + border-radius: 10px; + background-color: #8d99ae; + color: #000; + min-width: 30px; + min-height: 30px; + display: flex; + justify-content: center; + align-items: center; +} + +.exp-info-trial { + background-color: #e0e1dd; + border-radius: 10px; + padding: 10px; + margin: 10px 0; +} + +.exp-info-trial > h5 { + margin-bottom: 5px; +} + +.exp-info-trial table, .exp-info-trial td,.exp-info-trial th { + border: 1px solid gray; + text-align: left; +} + +.exp-info-trial table { + border-collapse: collapse; + width: 100%; +} + +.exp-info-trial th, .exp-info-trial td { + padding: 5px; +} +/* The Close Button */ +.close { + font-size: 2rem; + padding: 2px 10px; + border: none; + border-radius: 8px; + cursor: pointer; + transition: background-color 0.3s ease; + background-color: #8d99ae; + color: #000; +} + +.close:hover, +.close:focus { + background-color: #2980b9; + color: #fff; + text-decoration: none; + outline: none; +} + +/* Custom scroll bar */ +.modal-body::-webkit-scrollbar, .classification-experiment > ul::-webkit-scrollbar { + width: 10px; + border-radius: 10px; +} + +/* Track (the area the scrollbar sits on) */ +.modal-body::-webkit-scrollbar-track, .classification-experiment > ul::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +/* Handle (the draggable part of the scrollbar) */ +.modal-body::-webkit-scrollbar-thumb, .classification-experiment > ul::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; +} + +/* Handle on hover */ +.modal-body::-webkit-scrollbar-thumb:hover, .classification-experiment > ul::-webkit-scrollbar-thumb:hover { + background: #555; +} + diff --git a/app/controllers/classification_controller.rb b/app/controllers/classification_controller.rb new file mode 100644 index 00000000..8c6707f0 --- /dev/null +++ b/app/controllers/classification_controller.rb @@ -0,0 +1,2 @@ +class ClassificationController < ApplicationController +end diff --git a/app/controllers/experiment_controller.rb b/app/controllers/experiment_controller.rb new file mode 100644 index 00000000..1838ba19 --- /dev/null +++ b/app/controllers/experiment_controller.rb @@ -0,0 +1,230 @@ +# Purpose: Handle requests for experiments +class ExperimentController < ApplicationController + skip_forgery_protection + + # POST create experiment + # Params: + # experimentName: string + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # message: string + # experiment: { id: integer, name: string, disabled: boolean, created_at: datetime, updated_at: datetime } + # trials: [ + # { + # trial: { id: integer, name: string, disabled: boolean, deleted: boolean, runs: integer, experiment_id: integer, created_at: datetime, updated_at: datetime }, + # factors: [ + # { id: integer, name: string, value: string, created_at: datetime, updated_at: datetime }, + # ... + # ] + # }, + # ... + # ] + # error: string + # status: integer + def create + experiment_name = params['experimentName'] + factors = params['factors'] + return render_unprocessable_entity("Invalid params") unless create_params_are_valid(experiment_name, factors) + + experiment = save_experiment(experiment_name, factors) + + render_experiment_and_trials(experiment.id) + rescue StandardError => error + render_unprocessable_entity(error.message) + end + + + # GET all experiments + # Returns: + # experiments: [ + # { + # experiment: { id: integer, name: string, disabled: boolean, created_at: datetime, updated_at: datetime }, + # tags: [ + # { id: integer, name: string, color: string, created_at: datetime, updated_at: datetime }, + # ... + # ] + # }, + # ... + # ] + def get_all + experiments_with_tags = Experiment.includes(:experiment_tag => :tag) + + experiments = experiments_with_tags.map do |experiment| + experiment_tags = experiment.experiment_tag.map(&:tag) + { experiment: experiment, tags: experiment_tags } + end + + render json: { experiments: experiments }, status: :ok + end + + # POST add tag to experiment + # Params: + # experiment_id: integer + # tag_id: integer + # Returns: + # message: string + # experiment_tag: { id: integer, experiment_id: integer, tag_id: integer, created_at: datetime, updated_at: datetime } + # error: string + # status: integer + def add_tag + exp_id = params['experiment_id'] + tag_id = params['tag_id'] + + return render_not_found('Experiment not found') if Experiment.where(id: exp_id).empty? + + return render_not_found('Tag not found') if Tag.where(id: tag_id).empty? + + return render_unprocessable_entity('Experiment already has tag') \ + if ExperimentTag.where(experiment_id: exp_id, tag_id: tag_id).any? + + + # Add tag to experiment + experiment_tag = ExperimentTag.create(experiment_id: exp_id, tag_id: tag_id) + render json: { message: 'success', experiment_tag: experiment_tag }, status: :ok + + rescue StandardError => error + return render_unprocessable_entity(error.message) + end + + # DELETE remove tag from experiment + # Params: + # experiment_id: integer + # tag_id: integer + # Returns: + # message: string + # error: string + # status: integer + def remove_tag + exp_id = params['experiment_id'] + tag_id = params['tag_id'] + + return render_not_found('Experiment not found') if Experiment.where(id: exp_id).empty? + + return render_not_found('Tag not found') if Tag.where(id: tag_id).empty? + + experiment_tag = ExperimentTag.where(experiment_id: exp_id, tag_id: tag_id).first + + return render_unprocessable_entity('Experiment does not have tag') unless experiment_tag&.present? + + + experiment_tag.destroy + render json: { message: 'success' }, status: :ok + + rescue StandardError => error + render_unprocessable_entity(error.message) + end + + private + + # Check if params are valid + # Params: + # experiment_name: string + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # boolean + def create_params_are_valid(experiment_name, factors) + factors&.is_a?(ActionController::Parameters) \ + && factors.values.all? { |value| value.is_a?(Array) && value.length >= 1 } \ + && factors.keys.all? { |key| key.is_a?(String) } \ + && experiment_name.is_a?(String) \ + && experiment_name.length >= 1 + end + + def render_not_found(message) + render json: { error: message }, status: :not_found + end + + def render_unprocessable_entity(message) + render json: { error: message }, status: :unprocessable_entity + end + + # Save experiment and its factors and combinations + # Params: + # experiment_name: string + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # experiment: { id: integer, name: string, disabled: boolean, created_at: datetime, updated_at: datetime } + def save_experiment(experiment_name, factors) + experiment = Experiment.create(name: experiment_name, disabled: false) + save_factors(factors) + values = factors.values + save_combinations(experiment.id, factors.keys, values[0].product(*values[1..-1])) + return experiment + end + + # Save factors + # Params: + # factors: { factorName: [factorValue1, factorValue2, ...], ... } + # Returns: + # void + def save_factors(factors) + # Save factors (should not save if factor with same name and value already exists) + factors.each do |factor_name, factor_values| + save_factor(factor_name, factor_values) + end + end + + def save_factor(factor_name, factor_values) + factor_values.each do |value| + if Factor.where(name: factor_name, value: value).empty? + Factor.create(name: factor_name, value: value) + end + end + end + + # Save combinations + # Generate all possible combinations of factors and save them + # as [{factor1: value1, factor2: value1}, {factor1: value1, factor2: value2}, ...] + # Params: + # experiment_id: integer + # factors: [factorName1, factorName2, ...] + # values: [[factorValue1, factorValue2, ...], [factorValue1, factorValue2, ...], ...] + # Returns: + # void + def save_combinations(experiment_id, keys, combinations) + # combinations_keys looks like: + # [ [ [factor1: value1, factor2: value1], [factor1: value1, factor2: value2], ... ], + combinations_keys = combinations.map do |combination| + keys.zip(combination).map { |key, value| { key => value } } + end + + combinations_keys.each do |combination| + save_trial(experiment_id, combination) + end + end + + def save_trial(experiment_id, combination) + # Name is the combination of values e.g. value1-value2-value3 + name = combination.map { |factor| factor.values[0] }.join('-') + trial = Trial.create(name: name, disabled: false, deleted: false, runs: 0, experiment_id: experiment_id) + + combination.each do |value| + factor = Factor.where(name: value.keys[0], value: value.values[0]).first + TrialFactor.create(factor_id: factor.id, trial_id: trial.id) + end + end + + + def render_experiment_and_trials(experiment_id) + experiment = Experiment.find(experiment_id) + trials = get_trials(experiment_id) + render json: { + message: 'success', + experiment: experiment, + trials: trials + }, status: :ok + end + + def get_trials(experiment_id) + Trial.where(experiment_id: experiment_id).map do |trial| + factors = get_factors(trial.id) + { trial: trial, factors: factors } + end + end + + def get_factors(trial_id) + trial_factors = TrialFactor.where(trial_id: trial_id) + trial_factors.map { |trial_factor| Factor.find(trial_factor.factor_id) } + end + +end diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb new file mode 100644 index 00000000..ef6a8073 --- /dev/null +++ b/app/controllers/tags_controller.rb @@ -0,0 +1,16 @@ +class TagsController < ApplicationController + skip_forgery_protection + # get all tags + def get_all + render json: Tag.all + end + + def create + tag = Tag.new(params.require(:tag).permit(:name, :color)) + if tag.save + render json: tag + else + render json: tag.errors + end + end +end diff --git a/app/helpers/experiment_helper.rb b/app/helpers/experiment_helper.rb new file mode 100644 index 00000000..438b6654 --- /dev/null +++ b/app/helpers/experiment_helper.rb @@ -0,0 +1,2 @@ +module ExperimentHelper +end diff --git a/app/javascript/react/src/components/ClassificationExperiment.jsx b/app/javascript/react/src/components/ClassificationExperiment.jsx new file mode 100644 index 00000000..a572b35b --- /dev/null +++ b/app/javascript/react/src/components/ClassificationExperiment.jsx @@ -0,0 +1,165 @@ +import React, { useEffect, useState } from "react"; +import toast from "react-simple-toasts"; + +const ClassificationExperiment = () => { + const [experimentList, setExperimentList] = useState([]); + const [filteredExperiments, setFilteredExperiments] = useState([]); + const [tags, setTags] = useState([]); + const [searchText, setSearchText] = useState(""); + const [selectedTagId, setSelectedTagId] = useState(null); + + const fetchExperiments = async () => { + try { + const response = await fetch("/experiment/get_all"); + const data = (await response.json()).experiments; + setExperimentList(data); + setFilteredExperiments( + data.filter(({ experiment }) => experiment.name?.toLowerCase().includes(searchText)) + ); + } catch (error) { + console.error("Error fetching experiments:", error); + } + }; + + useEffect(() => { + fetchExperiments(); + + fetch("/tags/get_all") + .then((response) => response.json()) + .then((data) => { + setTags(data); + return data; + }) + .catch((error) => { + console.error("Error fetching tags:", error); + }); + }, []); + + const handleChange = (e) => { + const searchText = e.target.value.toLowerCase(); + setSearchText(searchText); + setFilteredExperiments( + experimentList.filter(({ experiment }) => experiment.name?.toLowerCase().includes(searchText)) + ); + }; + + const handleSelectedTagId = (tagId) => { + if (selectedTagId === tagId) { + setSelectedTagId(null); + } else { + setSelectedTagId(tagId); + } + }; + + const addTag = async (experimentId, tagId) => { + try { + const data = { experiment_id: experimentId, tag_id: parseInt(tagId) }; + const response = await fetch("/experiment/add_tag", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }).then(async (res) => { + const parse = JSON.parse(await res.text()); + return parse; + }); + + if (response?.error) { + toast("Erro ao adicionar tag!", { theme: "failure", position: "top-right" }); + return; + } + + fetchExperiments(); + + toast("Tag adicionada com sucesso!", { theme: "success", position: "top-right" }); + } catch (error) { + console.error("Error adding tag:", error); + toast("Erro ao adicionar tag!", { theme: "failure", position: "top-right" }); + } + }; + + const removeTag = async (experimentId, tagId) => { + try { + const data = { experiment_id: experimentId, tag_id: parseInt(tagId) }; + const response = await fetch("/experiment/remove_tag", { + method: "DELETE", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + }).then(async (res) => { + const parse = JSON.parse(await res.text()); + return parse; + }); + + if (response?.error) { + toast("Erro ao remover tag!", { theme: "failure", position: "top-right" }); + return; + } + + fetchExperiments(); + + toast("Tag removida com sucesso!", { theme: "success", position: "top-right" }); + } catch (error) { + console.error("Error removing tag:", error); + toast("Erro ao remover tag!", { theme: "failure", position: "top-right" }); + } + }; + + return ( +
+

Classificação de Experimentos

+ handleChange(e)} + /> +
+

Tags

+ +
+

Experimentos

+ +
+ ); +}; + +export default ClassificationExperiment; diff --git a/app/javascript/react/src/components/CreateExperiment.jsx b/app/javascript/react/src/components/CreateExperiment.jsx new file mode 100644 index 00000000..46005893 --- /dev/null +++ b/app/javascript/react/src/components/CreateExperiment.jsx @@ -0,0 +1,206 @@ +import React, { useEffect, useState } from "react"; +import toast from "react-simple-toasts"; +import { useClickAway } from "@uidotdev/usehooks"; + +const CreateExperiment = () => { + const [experimentName, setExperimentName] = useState( + localStorage.getItem("experimentName") || "" + ); + const [factors, setFactors] = useState(JSON.parse(localStorage.getItem("factors")) || {}); + const [value, setValue] = useState(""); + const [open, setOpen] = useState(null); + + const handleOpen = ({ experiment, trials }) => { + setOpen({ experiment, trials }); + }; + const handleClose = () => { + setOpen(null); + }; + const ref = useClickAway(handleClose); + + addFactor = (name) => { + if (name.length > 0 && !Object.keys(factors).includes(name)) { + setFactors({ ...factors, [name]: [] }); + } + }; + + removeFactor = (name) => { + const newFactors = { ...factors }; + delete newFactors[name]; + setFactors(newFactors); + }; + + addValueToFactor = (factorName, value) => { + const newFactors = { ...factors }; + if (newFactors[factorName] && !newFactors[factorName].includes(value) && value.length > 0) { + newFactors[factorName].push(value); + } + setFactors(newFactors); + }; + + removeValueFromFactor = (factorName, value) => { + const newFactors = { ...factors }; + if (newFactors[factorName]?.includes(value)) { + newFactors[factorName] = newFactors[factorName].filter((v) => v !== value); + } + setFactors(newFactors); + }; + + submitExperiment = () => { + const data = { experimentName, factors }; + + // post /experiments + fetch("/experiment/create", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(data), + credentials: "same-origin", + }) + .then(async (res) => { + const parse = JSON.parse(await res.text()); + return parse; + }) + .then((res) => { + if (res?.error) { + toast("Erro ao criar o experimento!", { + position: "top-right", + theme: "failure", + }); + return; + } + handleOpen(res); + toast("Experimento criado com sucesso!", { position: "top-right", theme: "success" }); + clearExperiment(); + }) + .catch((err) => { + toast("Erro ao criar o experimento!", { + position: "top-right", + theme: "failure", + }); + }); + }; + + clearExperiment = () => { + setExperimentName(""); + setFactors({}); + }; + + // Cache values in local storage so they are not lost on page refresh + useEffect(() => { + localStorage.setItem("experimentName", experimentName); + localStorage.setItem("factors", JSON.stringify(factors)); + }, [experimentName, factors]); + + return ( + <> + {/* Modal de experimento criado */} +
+
+
+

Experimento criado

+ +
+
+
+

{open?.experiment?.name}

+ {open?.experiment?.id} +
+

Ensaios

+ {open?.trials?.map((trial) => ( +
+
+

Ensaio {trial.trial.name}

+ {trial.trial.id} +
+
Fatores
+ + + {trial.factors.map((factor) => ( + + + + + ))} + +
{factor.name}{factor.value}
+
+ ))} +
+
+
+ {/* Fim do modal de experimento criado */} + + {/* Formulário de criação de experimento */} +
+
+

Criar Experimento

+
+ setExperimentName(e.target.value)} + /> +
+

Adicionar fator

+ + +

Valor para fator

+ setValue(e.target.value)} + /> + + Para adicionar o valor inserido ao fator, clique no botão 'Adicionar valor' ao lado do + nome do fator na seção 'Fatores'. + + + +
+
+
+

Fatores

+ {Object.keys(factors).map((factor) => ( +
+
+ {factor} +
+ + +
+
+ {factors[factor].map((value) => ( +
+ {value} + +
+ ))} +
+ ))} +
+
+ + ); +}; + +export default CreateExperiment; diff --git a/app/javascript/react/src/index.js b/app/javascript/react/src/index.js index a9759c53..68a83910 100644 --- a/app/javascript/react/src/index.js +++ b/app/javascript/react/src/index.js @@ -1,5 +1,7 @@ import { define } from 'remount' import Hello from "./components/Hello" import Graph from "./components/Graph" +import CreateExperiment from "./components/CreateExperiment" +import ClassificationExperiment from './components/ClassificationExperiment' -define({ 'hello-component': Hello, 'graph-component': Graph }) +define({ 'hello-component': Hello, 'graph-component': Graph, 'create-experiment-component': CreateExperiment, 'create-classification-experiment-component': ClassificationExperiment }) \ No newline at end of file diff --git a/app/models/experiment.rb b/app/models/experiment.rb index 5cf63272..e7f5e492 100644 --- a/app/models/experiment.rb +++ b/app/models/experiment.rb @@ -1,3 +1,5 @@ class Experiment < ApplicationRecord has_many :trials + has_many :experiment_tag + has_many :tags, through: :experiment_tag end diff --git a/app/models/experiment_tag.rb b/app/models/experiment_tag.rb new file mode 100644 index 00000000..8f6879bc --- /dev/null +++ b/app/models/experiment_tag.rb @@ -0,0 +1,4 @@ +class ExperimentTag < ApplicationRecord + belongs_to :experiment + belongs_to :tag +end diff --git a/app/models/tag.rb b/app/models/tag.rb index 98eba85b..5843a35c 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -1,3 +1,4 @@ class Tag < ApplicationRecord has_many :trials, through: :classification + has_many :experiments, through: :experiment_tag end diff --git a/app/views/classification/index.html.erb b/app/views/classification/index.html.erb new file mode 100644 index 00000000..b9c62a6e --- /dev/null +++ b/app/views/classification/index.html.erb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/views/experiment/index.html.erb b/app/views/experiment/index.html.erb new file mode 100644 index 00000000..e3a951c7 --- /dev/null +++ b/app/views/experiment/index.html.erb @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/bun.lockb b/bun.lockb index fbc672dd..17db9e53 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/config/routes.rb b/config/routes.rb index a27857a6..ab880e20 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,15 @@ Rails.application.routes.draw do + resources :create_experiments get 'hello/world' - get 'hello/worldGet', to: "hello#index" + get 'hello/worldGet', to: "hello#index" + get 'experiment/create', to: "experiment#index" + post 'experiment/create', to: "experiment#create" + get 'experiment/classification', to: "classification#index" + get 'experiment/get_all', to: "experiment#get_all" + post 'tags/create', to: "tags#create" + get 'tags/get_all', to: "tags#get_all" + post 'experiment/add_tag', to: "experiment#add_tag" + delete 'experiment/remove_tag', to: "experiment#remove_tag" # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html # Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500. diff --git a/db/migrate/20231203174203_create_experiment_tags.rb b/db/migrate/20231203174203_create_experiment_tags.rb new file mode 100644 index 00000000..204f79d8 --- /dev/null +++ b/db/migrate/20231203174203_create_experiment_tags.rb @@ -0,0 +1,9 @@ +class CreateExperimentTags < ActiveRecord::Migration[7.1] + def change + create_table :experiment_tags do |t| + t.belongs_to :experiment + t.belongs_to :tag + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 183d56d5..df8612a9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2023_11_13_225719) do +ActiveRecord::Schema[7.1].define(version: 2023_12_03_174203) do create_table "classifications", force: :cascade do |t| t.integer "trial_id" t.integer "tag_id" @@ -20,6 +20,15 @@ t.index ["trial_id"], name: "index_classifications_on_trial_id" end + create_table "experiment_tags", force: :cascade do |t| + t.integer "experiment_id" + t.integer "tag_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["experiment_id"], name: "index_experiment_tags_on_experiment_id" + t.index ["tag_id"], name: "index_experiment_tags_on_tag_id" + end + create_table "experiments", force: :cascade do |t| t.string "name" t.boolean "disabled" @@ -44,8 +53,10 @@ create_table "trial_executions", force: :cascade do |t| t.string "status" t.text "log" + t.integer "trial_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.index ["trial_id"], name: "index_trial_executions_on_trial_id" end create_table "trial_factors", force: :cascade do |t| diff --git a/features/4_Adicionar_experimento.feature b/features/4_Adicionar_experimento.feature new file mode 100644 index 00000000..ece7753b --- /dev/null +++ b/features/4_Adicionar_experimento.feature @@ -0,0 +1,26 @@ + Feature: Add a new experiment and Confirmation + @javascript + Scenario: Add a new experiment (Happy) + When the user is on the add experiment screen + And the user clicks on the experiment name field and types "Experiment B1" + And the user clicks on the factor name field and types "Velocidade" + And the user clicks on the add factor button + And the user clicks on the factor name field and types "Temperatura" + And the user clicks on the add factor button + And the user clicks on the factor value field and types "10" + And the user clicks on the button with id "add-to-Velocidade" + And the user clicks on the factor value field and types "20" + And the user clicks on the button with id "add-to-Velocidade" + And the user clicks on the factor value field and types "30" + And the user clicks on the button with id "add-to-Temperatura" + And the user clicks on the factor value field and types "40" + And the user clicks on the button with id "add-to-Temperatura" + And the user clicks on the create experiment button + Then the user should see a modal with the experiment details + + @javascript + Scenario: Add a new experiment (Sad) + When the user is on the add experiment screen + And the user dont fill the fields + And the user clicks on the create experiment button + Then the user should see the fail message "Erro ao criar o experimento!" diff --git a/features/5_Classificar_experimentos.feature b/features/5_Classificar_experimentos.feature new file mode 100644 index 00000000..4a06870b --- /dev/null +++ b/features/5_Classificar_experimentos.feature @@ -0,0 +1,14 @@ +Feature: Classify experiments with tags + @javascript + Scenario: Add tag to experiment (Happy) + Given I am on the "/experiment/classification" page + When I click on the tag with id "tag-2" + And I click on the add button with id "add-tag-1" + Then I should see the message "Tag adicionada com sucesso!" + And I should see the tag "Tag 2" on the experiment "experiment-1-tags" + + @javascript + Scenario: Add tag to experiment without selecting a tag (Sad) + Given I am on the "/experiment/classification" page + And I click on the add button with id "add-tag-1" + Then I should see the message "Erro ao adicionar tag!" \ No newline at end of file diff --git a/features/step_definitions/4_Adicionar_experimento_step.rb b/features/step_definitions/4_Adicionar_experimento_step.rb new file mode 100644 index 00000000..bd3172eb --- /dev/null +++ b/features/step_definitions/4_Adicionar_experimento_step.rb @@ -0,0 +1,46 @@ +When("the user is on the add experiment screen") do + visit '/experiment/create' +end + +And("the user clicks on the experiment name field and types {string}") do |string| + fill_in 'experimentName', with: string +end + +And("the user clicks on the factor name field and types {string}") do |string| + fill_in 'factorName', with: string +end + +And("the user clicks on the add factor button") do + click_button 'addFactor' +end + +And("the user clicks on the factor value field and types {string}") do |string| + fill_in 'factorValue', with: string +end + +And("the user clicks on the button with id {string}") do |string| + find(:css, '#' + string).click +end + +And("the user clicks on the create experiment button") do + click_button 'createExperiment' +end + +Then('the user should see a modal with the experiment details') do + expect(page).to have_content('Experimento criado') + expect(page).to have_content('Experiment B1') + expect(page).to have_content('Ensaio 10-30') + expect(page).to have_content('Ensaio 10-40') + expect(page).to have_content('Ensaio 20-30') + expect(page).to have_content('Ensaio 20-40') +end + +And("the user dont fill the fields") do + fill_in 'experimentName', with: '' + fill_in 'factorName', with: '' + fill_in 'factorValue', with: '' +end + +Then("the user should see the fail message {string}") do |string| + expect(page).to have_content(string) +end diff --git a/features/step_definitions/5_Classificar_experimentos_step.rb b/features/step_definitions/5_Classificar_experimentos_step.rb new file mode 100644 index 00000000..dc1969cc --- /dev/null +++ b/features/step_definitions/5_Classificar_experimentos_step.rb @@ -0,0 +1,26 @@ +Before do + Experiment.create(name: 'Experimento 1') + Tag.create(name: 'Tag 1', color: 'red') + Tag.create(name: 'Tag 2', color: 'blue') +end + +Given("I am on the {string} page") do |string| + visit string +end + +When("I click on the tag with id {string}") do |string| + find(:css, '#' + string).click +end + +And("I click on the add button with id {string}") do |string| + find(:css, '#' + string).click +end + + +Then("I should see the message {string}") do |string| + expect(page).to have_content(string) +end + +And("I should see the tag {string} on the experiment {string}") do |string, string2| + expect(page).to have_css('#'+string2, text: string) +end diff --git a/package.json b/package.json index 25b33f79..94a92bb1 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,10 @@ "dependencies": { "@hotwired/stimulus": "^3.2.2", "@hotwired/turbo-rails": "^7.3.0", + "@uidotdev/usehooks": "^2.4.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-simple-toasts": "^5.10.0", "remount": "^1.0.0" } } \ No newline at end of file diff --git a/spec/controllers/experiment_controller_spec.rb b/spec/controllers/experiment_controller_spec.rb new file mode 100644 index 00000000..a13da65f --- /dev/null +++ b/spec/controllers/experiment_controller_spec.rb @@ -0,0 +1,220 @@ +require 'rails_helper' + +RSpec.describe ExperimentController, type: :controller do + context 'POST #create' do + it 'should create an experiment' do + post :create, params: { experimentName: 'test', factors: { 'velocidade' => ['1', '2'], 'temperatura' => ['4', '5'] } } + + + body = JSON.parse(response.body) + expect(body['experiment']['name']).to eq('test') + expect(body['experiment']['disabled']).to eq(false) + expect(body['trials'].length).to eq(4) + + body['trials'].each do |trial| + expect(trial['trial']['disabled']).to eq(false) + expect(trial['trial']['deleted']).to eq(false) + expect(trial['trial']['runs']).to eq(0) + expect(trial['trial']['experiment_id']).to eq(body['experiment']['id']) + end + + trial_names = body['trials'].map { |trial| trial['trial']['name'] } + expect(trial_names).to include('4-1', '4-2', '5-1', '5-2').or include('1-4', '1-5', '2-4', '2-5') + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('success') + end + + it 'should not create an experiment if experimentName is nil' do + post :create, params: {factors: { 'velocidade' => ['1', '2'], 'temperatura' => ['4', '5'] } } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not create an experiment if experimentName is empty' do + post :create, params: { experimentName: '', factors: { 'velocidade' => ['1', '2'], 'temperatura' => ['4', '5'] } } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not create an experiment if factors is nil' do + post :create, params: { experimentName: 'test' } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not create an experiment if factors is empty' do + post :create, params: { experimentName: 'test', factors: {} } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not create an experiment if factors array is empty' do + post :create, params: { experimentName: 'test', factors: { 'velocidade' => [], 'temperatura' => [] } } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not create an experiment if factors is not a hash' do + post :create, params: { experimentName: 'test', factors: 'velocidade' } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + + it 'should not create an experiment if factors is a hash with values that are not arrays' do + post :create, params: { experimentName: 'test', factors: { 'velocidade' => '1', 'temperatura' => ['4', '5'] } } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Invalid params') + end + end + + context 'POST #add_tag' do + # setup + before(:each) do + Experiment.create(id: 1, name: 'test') + Tag.create(id: 1, name: 'tag', color: 'red') + end + + it 'should add tag to experiment' do + post :add_tag, params: { experiment_id: 1, tag_id: 1 } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('success') + end + + it 'should not add tag to experiment if experiment_id is nil' do + post :add_tag, params: { tag_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') + end + + it 'should not add tag to experiment if tag_id is nil' do + post :add_tag, params: { experiment_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') + end + + it 'should not add tag to experiment if experiment does not exist' do + post :add_tag, params: { experiment_id: 2, tag_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') + end + + it 'should not add tag to experiment if tag does not exist' do + post :add_tag, params: { experiment_id: 1, tag_id: 2 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') + end + + it 'should not add tag to experiment if experiment already has tag' do + ExperimentTag.create(experiment_id: 1, tag_id: 1) + post :add_tag, params: { experiment_id: 1, tag_id: 1 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Experiment already has tag') + end + + end + + context 'DELETE #remove_tag' do + + # setup + before(:each) do + Experiment.create(id: 1, name: 'test') + Tag.create(id: 1, name: 'tag', color: 'red') + ExperimentTag.create(experiment_id: 1, tag_id: 1) + Experiment.create(id: 2, name: 'test2') + Tag.create(id: 2, name: 'tag2', color: 'blue') + end + + it 'should remove tag from experiment' do + delete :remove_tag, params: { experiment_id: 1, tag_id: 1 } + + expect(response).to have_http_status(:ok) + expect(JSON.parse(response.body)['message']).to eq('success') + + expect(ExperimentTag.where(experiment_id: 1, tag_id: 1).any?).to eq(false) + end + + it 'should not remove tag from experiment if experiment_id is nil' do + delete :remove_tag, params: { tag_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') + end + + it 'should not remove tag from experiment if tag_id is nil' do + delete :remove_tag, params: { experiment_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') + end + + it 'should not remove tag from experiment if experiment does not exist' do + delete :remove_tag, params: { experiment_id: 3, tag_id: 1 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Experiment not found') + end + + it 'should not remove tag from experiment if tag does not exist' do + delete :remove_tag, params: { experiment_id: 1, tag_id: 3 } + + expect(response).to have_http_status(:not_found) + expect(JSON.parse(response.body)['error']).to eq('Tag not found') + end + + it 'should not remove tag from experiment if experiment does not have tag' do + delete :remove_tag, params: { experiment_id: 2, tag_id: 2 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Experiment does not have tag') + end + + it 'should not remove tag from experiment if experiment has tag but not the one being removed' do + delete :remove_tag, params: { experiment_id: 1, tag_id: 2 } + + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['error']).to eq('Experiment does not have tag') + end + end + + context 'GET #get_all' do + # setup + before(:each) do + Experiment.create(id: 1, name: 'test', disabled: false) + Factor.create(id: 1, name: 'velocidade', value: '1') + Trial.create(id: 1, name: '1', experiment_id: 1, disabled: false, deleted: false, runs: 0) + TrialFactor.create(id: 1, trial_id: 1, factor_id: 1) + Tag.create(id: 1, name: 'tag', color: 'red') + ExperimentTag.create(id: 1, experiment_id: 1, tag_id: 1) + end + + it 'should get all experiments' do + get :get_all + + expect(response).to have_http_status(:ok) + experiments = JSON.parse(response.body)['experiments'] + + expect(experiments.length).to eq(1) + experiment = experiments[0]['experiment'] + expect(experiment['id']).to eq(1) + + tags = experiments[0]['tags'] + expect(tags.length).to eq(1) + tag = tags[0] + expect(tag['id']).to eq(1) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 00000000..a15455f3 --- /dev/null +++ b/spec/rails_helper.rb @@ -0,0 +1,65 @@ +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require_relative '../config/environment' +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Rails.root.glob('spec/support/**/*.rb').sort.each { |f| require f } + +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + abort e.to_s.strip +end +RSpec.configure do |config| + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_paths = [ + Rails.root.join('spec/fixtures') + ] + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://rspec.info/features/6-0/rspec-rails + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 00000000..93399d18 --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,96 @@ +require 'simplecov' +SimpleCov.start +# This file was generated by the `rails generate rspec:install` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + +# The settings below are suggested to provide a good initial experience +# with RSpec, but feel free to customize to your heart's content. +=begin + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # https://rspec.info/features/3-12/rspec-core/configuration/zero-monkey-patching-mode/ + config.disable_monkey_patching! + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + if config.files_to_run.one? + # Use the documentation formatter for detailed output, + # unless a formatter has already been configured + # (e.g. via a command-line flag). + config.default_formatter = "doc" + end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed +=end +end diff --git a/test/controllers/experiment_controller_test.rb b/test/controllers/experiment_controller_test.rb new file mode 100644 index 00000000..4e9b0163 --- /dev/null +++ b/test/controllers/experiment_controller_test.rb @@ -0,0 +1,9 @@ +require "test_helper" + +class ExperimentControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end + + +end diff --git a/test/integration/4_Adicionar_experimento.feature b/test/integration/4_Adicionar_experimento.feature new file mode 100644 index 00000000..e56cfe7e --- /dev/null +++ b/test/integration/4_Adicionar_experimento.feature @@ -0,0 +1,44 @@ +Funcionalidade: Criar um experimento baseado em planos locais + +Contexto: +Dado que sou um usuário +E estou logado na plataforma +E estou na tela para adicionar um novo experimento +E não existe nenhum robô + +Cenário: Registrar um novo robô (Happy) +Dado que o usuário clique no botão adicionar um novo robô +Quando clico para registra o parâmetro nome e confirmo +Então um novo robô é registrado com sucesso. + +Cenário: Registrar um novo robô (Sad) +Dado que o usuário clique no botão adicionar um novo robô +Quando clico para adicionar o parâmetro nome e confirmo +Então o robô não é adicionado devido a um erro ou à sintaxe inválida dos parâmetros. + +Contexto: +Dado que sou um usuário +E estou logado na plataforma +E estou na tela para adicionar um novo experimento +E já existem um ou mais robôs + +Cenário: Registrar um novo robô (Sad - Robô Existente) +Dado que o usuário deseja registrar um novo robô +Quando clico para adicionar o parâmetro nome e confirmo +Então o robô não é adicionado devido a um erro ou ao fornecimento de um nome já existente. + +Contexto: +Dado que sou um usuário +E estou logado na plataforma +E estou na tela para adicionar um novo experimento +E um robô já existe. + +Cenário: Registrar um novo experimento (Happy) +Dado que o usuário deseja adicionar um novo experimento +Quando forneço os parâmetros corretamente e especifico a quantidade de robôs +Então os testes são gerados como esperado. + +Cenário: Registrar um novo experimento (Sad) +Dado que o usuário deseja adicionar um novo experimento +Quando forneço os parâmetros de forma inválida e especifico a quantidade de robôs +Então os testes não são gerados como esperado. diff --git a/test/integration/5_Classificar_experimento.feature b/test/integration/5_Classificar_experimento.feature new file mode 100644 index 00000000..66e9662f --- /dev/null +++ b/test/integration/5_Classificar_experimento.feature @@ -0,0 +1,21 @@ +Funcionalidade: Classificar experimentos com uma tag + +Contexto: +Dado que sou um usuário +E estou logado na plataforma +E estou na tela de lista dos experimentos + +Cenário: Classificar um experimento (Happy) +Dado que eu desejo classificar um novo experimento +Quando seleciono o experimento desejado e atribuo o nome da tag +Então o experimento é classificado com sucesso e a cor é atribuída ao experimento. + +Cenário: Classificar um experimento (Sad) +Dado que eu desejo classificar um novo experimento +Quando seleciono o experimento desejado e atribuo o nome da tag +Então o experimento não pode ser classificado, pois a tag não existe. + +Cenário: Classificar um experimento (Sad) +Dado que eu desejo classificar um novo experimento +Quando seleciono o experimento desejado e atribuo um nome de tag inválido +Então o experimento não pode ser classificado devido à tag inválida.