Skip to content

Commit f4db42d

Browse files
committed
fix: remove requester fields from form, use FormElement, enforce https URL
- Drop requester name/email inputs (now filled server-side) - Replace bootstrap inputs with Galaxy's FormElement for consistent UI - Validate tool URL: only https:// links accepted - Update tests to use suppressExpectedErrorMessages helper instead of silencing all console errors
1 parent 2d41858 commit f4db42d

2 files changed

Lines changed: 131 additions & 110 deletions

File tree

client/src/components/Tool/ToolRequestForm.test.ts

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { getLocalVue } from "@tests/vitest/helpers";
1+
import "@/composables/__mocks__/filter";
2+
3+
import { createTestingPinia } from "@pinia/testing";
4+
import { getLocalVue, suppressExpectedErrorMessages } from "@tests/vitest/helpers";
25
import { mount, type Wrapper } from "@vue/test-utils";
36
import flushPromises from "flush-promises";
47
import { beforeEach, describe, expect, it, vi } from "vitest";
@@ -17,26 +20,27 @@ vi.mock("@/api/toolRequestForm", () => ({
1720
const localVue = getLocalVue(true);
1821

1922
async function mountForm(show = true): Promise<Wrapper<Vue>> {
20-
// Suppress FormBoolean null-value prop warnings — condaAvailable and testDataAvailable
21-
// are intentionally null until the user interacts with them.
22-
vi.spyOn(console, "error").mockImplementation(() => {});
23+
// Suppress only the known BootstrapVue prop warnings for null boolean values
24+
// (condaAvailable and testDataAvailable are intentionally null until the user interacts).
25+
suppressExpectedErrorMessages(["Invalid prop: type check failed for prop"]);
2326

27+
const pinia = createTestingPinia({ createSpy: vi.fn });
2428
const wrapper = mount(ToolRequestForm as object, {
2529
localVue,
2630
propsData: { show },
31+
pinia,
2732
attachTo: document.body,
2833
});
2934
await flushPromises();
3035
return wrapper;
3136
}
3237

33-
/** Fill the three required fields: tool_name, description, requester_name. */
38+
/** Fill the two required fields: tool_name and description. */
3439
async function fillRequiredFields(wrapper: Wrapper<Vue>, overrides: Record<string, string> = {}) {
3540
await wrapper.find("#tool-request-name").setValue(overrides["tool_name"] ?? "FastQC");
3641
await wrapper
3742
.find("#tool-request-description")
3843
.setValue(overrides["description"] ?? "Quality control for sequencing data");
39-
await wrapper.find("#tool-request-requester-name").setValue(overrides["requester_name"] ?? "Dr. Smith");
4044
await flushPromises();
4145
}
4246

@@ -61,9 +65,11 @@ describe("ToolRequestForm", () => {
6165
const wrapper = await mountForm(true);
6266
expect(wrapper.find("#tool-request-name").exists()).toBe(true);
6367
expect(wrapper.find("#tool-request-description").exists()).toBe(true);
64-
expect(wrapper.find("#tool-request-requester-name").exists()).toBe(true);
6568
expect(wrapper.find("#tool-request-url").exists()).toBe(true);
66-
expect(wrapper.find("#tool-request-requester-email").exists()).toBe(true);
69+
expect(wrapper.find("#tool-request-requester-affiliation").exists()).toBe(true);
70+
// Name and email are filled server-side; no fields for them in the form
71+
expect(wrapper.find("#tool-request-requester-name").exists()).toBe(false);
72+
expect(wrapper.find("#tool-request-requester-email").exists()).toBe(false);
6773
});
6874

6975
it("ok button is disabled when required fields are empty", async () => {
@@ -87,8 +93,6 @@ describe("ToolRequestForm", () => {
8793
await wrapper.find("#tool-request-description").setValue("Quality control for sequencing data");
8894
await wrapper.find("#tool-request-domain").setValue("Genomics");
8995
await wrapper.find("#tool-request-version").setValue("0.12.1");
90-
await wrapper.find("#tool-request-requester-name").setValue("Dr. Smith");
91-
await wrapper.find("#tool-request-requester-email").setValue("smith@example.com");
9296
await wrapper.find("#tool-request-requester-affiliation").setValue("Example University");
9397
await flushPromises();
9498

@@ -104,10 +108,21 @@ describe("ToolRequestForm", () => {
104108
description: "Quality control for sequencing data",
105109
scientific_domain: "Genomics",
106110
requested_version: "0.12.1",
107-
requester_name: "Dr. Smith",
108-
requester_email: "smith@example.com",
109111
requester_affiliation: "Example University",
110112
});
113+
// Requester name and email come from the server (authenticated user), not the form
114+
expect(payload).not.toHaveProperty("requester_name");
115+
expect(payload).not.toHaveProperty("requester_email");
116+
});
117+
118+
it("rejects non-https URL and does not submit", async () => {
119+
const wrapper = await mountForm(true);
120+
await fillRequiredFields(wrapper);
121+
await wrapper.find("#tool-request-url").setValue("http://example.com/tool");
122+
wrapper.findComponent(GModal).vm.$emit("ok");
123+
await flushPromises();
124+
expect(mockSubmitToolRequest).not.toHaveBeenCalled();
125+
expect(wrapper.text()).toContain("https://");
111126
});
112127

113128
it("shows success alert after successful submission", async () => {
@@ -131,6 +146,7 @@ describe("ToolRequestForm", () => {
131146

132147
it("does not call API when required fields are missing", async () => {
133148
const wrapper = await mountForm(true);
149+
// Only fill tool_name, leave description empty
134150
await wrapper.find("#tool-request-name").setValue("Samtools");
135151
await flushPromises();
136152
wrapper.findComponent(GModal).vm.$emit("ok");
Lines changed: 103 additions & 98 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
<script setup lang="ts">
2-
import { BAlert, BFormGroup, BFormInput, BFormSelect, BFormTextarea } from "bootstrap-vue";
2+
import { BAlert } from "bootstrap-vue";
33
import { ref } from "vue";
44
55
import { submitToolRequest } from "@/api/toolRequestForm";
66
import { errorMessageAsString } from "@/utils/simple-error";
77
88
import GModal from "@/components/BaseComponents/GModal.vue";
9+
import FormElement from "@/components/Form/FormElement.vue";
910
1011
const props = defineProps<{
1112
show: boolean;
@@ -22,15 +23,34 @@ const scientificDomain = ref("");
2223
const requestedVersion = ref("");
2324
const condaAvailable = ref<boolean | null>(null);
2425
const testDataAvailable = ref<boolean | null>(null);
25-
const requesterName = ref("");
26-
const requesterEmail = ref("");
2726
const requesterAffiliation = ref("");
2827
2928
const submitting = ref(false);
3029
const successMessage = ref("");
3130
const errorMessage = ref("");
31+
const urlError = ref("");
3232
33-
const formValid = () => !!(toolName.value.trim() && description.value.trim() && requesterName.value.trim());
33+
const formValid = () => !!(toolName.value.trim() && description.value.trim());
34+
35+
function validateUrl(): boolean {
36+
const url = toolUrl.value.trim();
37+
if (!url) {
38+
urlError.value = "";
39+
return true;
40+
}
41+
try {
42+
const parsed = new URL(url);
43+
if (parsed.protocol !== "https:") {
44+
urlError.value = "Only https:// URLs are allowed.";
45+
return false;
46+
}
47+
} catch {
48+
urlError.value = "Please enter a valid URL (e.g. https://example.com).";
49+
return false;
50+
}
51+
urlError.value = "";
52+
return true;
53+
}
3454
3555
function resetForm() {
3656
toolName.value = "";
@@ -40,10 +60,9 @@ function resetForm() {
4060
requestedVersion.value = "";
4161
condaAvailable.value = null;
4262
testDataAvailable.value = null;
43-
requesterName.value = "";
44-
requesterEmail.value = "";
4563
requesterAffiliation.value = "";
4664
errorMessage.value = "";
65+
urlError.value = "";
4766
successMessage.value = "";
4867
}
4968
@@ -58,6 +77,10 @@ async function submit() {
5877
return;
5978
}
6079
80+
if (!validateUrl()) {
81+
return;
82+
}
83+
6184
submitting.value = true;
6285
errorMessage.value = "";
6386
successMessage.value = "";
@@ -71,8 +94,6 @@ async function submit() {
7194
requested_version: requestedVersion.value.trim() || undefined,
7295
conda_available: condaAvailable.value ?? undefined,
7396
test_data_available: testDataAvailable.value ?? undefined,
74-
requester_name: requesterName.value.trim(),
75-
requester_email: requesterEmail.value.trim() || undefined,
7697
requester_affiliation: requesterAffiliation.value.trim() || undefined,
7798
});
7899
@@ -115,102 +136,86 @@ async function submit() {
115136

116137
<h6 v-localize class="font-weight-bold mb-2">Tool Information</h6>
117138

118-
<BFormGroup label="Tool Name *" label-for="tool-request-name">
119-
<BFormInput
120-
id="tool-request-name"
121-
v-model="toolName"
122-
placeholder="e.g. FastQC"
123-
:disabled="submitting"
124-
required />
125-
</BFormGroup>
126-
127-
<BFormGroup label="Homepage / Repository URL" label-for="tool-request-url">
128-
<BFormInput
129-
id="tool-request-url"
130-
v-model="toolUrl"
131-
placeholder="e.g. https://github.com/..."
132-
:disabled="submitting" />
133-
</BFormGroup>
134-
135-
<BFormGroup label="Description *" label-for="tool-request-description">
136-
<BFormTextarea
137-
id="tool-request-description"
138-
v-model="description"
139-
placeholder="Describe the tool and its scientific use case"
140-
rows="3"
141-
:disabled="submitting"
142-
required />
143-
</BFormGroup>
144-
145-
<BFormGroup label="Scientific Domain" label-for="tool-request-domain">
146-
<BFormInput
147-
id="tool-request-domain"
148-
v-model="scientificDomain"
149-
placeholder="e.g. Genomics, Proteomics, AI/ML"
150-
:disabled="submitting" />
151-
</BFormGroup>
152-
153-
<BFormGroup label="Requested Version" label-for="tool-request-version">
154-
<BFormInput
155-
id="tool-request-version"
156-
v-model="requestedVersion"
157-
placeholder="e.g. 1.2.0"
158-
:disabled="submitting" />
159-
</BFormGroup>
139+
<FormElement
140+
id="tool-request-name"
141+
v-model="toolName"
142+
type="text"
143+
title="Tool Name"
144+
help="e.g. FastQC"
145+
:attributes="{ optional: false }" />
146+
147+
<FormElement
148+
id="tool-request-url"
149+
v-model="toolUrl"
150+
type="text"
151+
title="Homepage / Repository URL"
152+
help="e.g. https://github.com/..."
153+
:error="urlError" />
154+
155+
<FormElement
156+
id="tool-request-description"
157+
v-model="description"
158+
type="text"
159+
title="Description"
160+
help="Describe the tool and its scientific use case"
161+
:attributes="{ area: true, optional: false }" />
162+
163+
<FormElement
164+
id="tool-request-domain"
165+
v-model="scientificDomain"
166+
type="text"
167+
title="Scientific Domain"
168+
help="e.g. Genomics, Proteomics, AI/ML" />
169+
170+
<FormElement
171+
id="tool-request-version"
172+
v-model="requestedVersion"
173+
type="text"
174+
title="Requested Version"
175+
help="e.g. 1.2.0" />
160176

161177
<div class="d-flex gap-3 mb-3">
162-
<BFormGroup label="Conda package available?" label-for="tool-request-conda" class="flex-fill mb-0">
163-
<BFormSelect
164-
id="tool-request-conda"
165-
v-model="condaAvailable"
166-
:options="[
167-
{ value: null, text: 'Not specified' },
168-
{ value: true, text: 'Yes' },
169-
{ value: false, text: 'No' },
170-
]"
171-
:disabled="submitting" />
172-
</BFormGroup>
173-
174-
<BFormGroup label="Test data available?" label-for="tool-request-test-data" class="flex-fill mb-0">
175-
<BFormSelect
176-
id="tool-request-test-data"
177-
v-model="testDataAvailable"
178-
:options="[
179-
{ value: null, text: 'Not specified' },
180-
{ value: true, text: 'Yes' },
181-
{ value: false, text: 'No' },
182-
]"
183-
:disabled="submitting" />
184-
</BFormGroup>
178+
<FormElement
179+
id="tool-request-conda"
180+
v-model="condaAvailable"
181+
type="select"
182+
title="Conda package available?"
183+
class="flex-fill"
184+
:attributes="{
185+
data: [
186+
{ label: 'Not specified', value: null },
187+
{ label: 'Yes', value: true },
188+
{ label: 'No', value: false },
189+
],
190+
}" />
191+
192+
<FormElement
193+
id="tool-request-test-data"
194+
v-model="testDataAvailable"
195+
type="select"
196+
title="Test data available?"
197+
class="flex-fill"
198+
:attributes="{
199+
data: [
200+
{ label: 'Not specified', value: null },
201+
{ label: 'Yes', value: true },
202+
{ label: 'No', value: false },
203+
],
204+
}" />
185205
</div>
186206

187207
<h6 v-localize class="font-weight-bold mb-2 mt-3">Requester Information</h6>
188208

189-
<BFormGroup label="Name *" label-for="tool-request-requester-name">
190-
<BFormInput
191-
id="tool-request-requester-name"
192-
v-model="requesterName"
193-
placeholder="Your name"
194-
:disabled="submitting"
195-
required />
196-
</BFormGroup>
197-
198-
<BFormGroup label="Email (for follow-up)" label-for="tool-request-requester-email">
199-
<BFormInput
200-
id="tool-request-requester-email"
201-
v-model="requesterEmail"
202-
type="email"
203-
placeholder="your@email.com"
204-
:disabled="submitting" />
205-
</BFormGroup>
206-
207-
<BFormGroup label="Affiliation / Lab" label-for="tool-request-requester-affiliation">
208-
<BFormInput
209-
id="tool-request-requester-affiliation"
210-
v-model="requesterAffiliation"
211-
placeholder="Your institution or lab"
212-
:disabled="submitting" />
213-
</BFormGroup>
209+
<p class="mb-2 text-muted small">
210+
Your account name and email will be automatically included in the request.
211+
</p>
212+
213+
<FormElement
214+
id="tool-request-requester-affiliation"
215+
v-model="requesterAffiliation"
216+
type="text"
217+
title="Affiliation / Lab"
218+
help="Your institution or lab" />
214219
</div>
215220
</GModal>
216221
</template>

0 commit comments

Comments
 (0)