Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions frontend/e2e/specs/rating-field.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { test, expect } from "../fixtures/test-data.fixture";
import { SubmissionPage } from "../helpers/submission";

test.describe("Rating field", () => {
test("retains the value the user clicked (no clamp to 1.0)", async ({
browser,
createPublishedForm,
apiContext,
}) => {
const { formId, route } = await createPublishedForm();

// Add a Rating field to the published form via REST
const formRes = await apiContext.get(`/api/resource/Form/${formId}`);
const { data: formData } = await formRes.json();
const linkedDoctype: string = formData.linked_doctype;
const ratingFieldname = "satisfaction";

await apiContext.put(`/api/resource/Form/${formId}`, {
data: {
fields: [
...(formData.fields ?? []),
{
fieldtype: "Rating",
label: "Satisfaction",
fieldname: ratingFieldname,
reqd: 0,
},
],
},
});

// Guest fills + submits the form
const guestCtx = await browser.newContext();
const guestPage = await guestCtx.newPage();
const submissionPage = new SubmissionPage(guestPage);
await submissionPage.goto(route);

await expect(guestPage.getByRole("button", { name: "Submit" })).toBeVisible(
{ timeout: 10000 }
);

// Click the 3rd star (1-indexed → nth(2)). feather-icons stamps the class.
const stars = guestPage.locator("svg.feather-star");
await expect(stars).toHaveCount(5);
await stars.nth(2).click();

await submissionPage.submit();
await expect(submissionPage.successMessage()).toBeVisible({
timeout: 10000,
});

await guestCtx.close();

// Fetch the submission record via REST and assert the stored value.
// Frappe stores Rating as a 0..1 fraction → 3/5 == 0.6.
const listRes = await apiContext.get(
`/api/resource/${encodeURIComponent(linkedDoctype)}`,
{
params: {
filters: JSON.stringify([["fp_linked_form", "=", formId]]),
fields: JSON.stringify(["name"]),
limit_page_length: 1,
},
}
);
const { data: list } = await listRes.json();
expect(list).toHaveLength(1);
const submissionName = list[0].name;

const getRes = await apiContext.get(
`/api/resource/${encodeURIComponent(linkedDoctype)}/${encodeURIComponent(
submissionName
)}`
);
const { data: submission } = await getRes.json();
expect(submission[ratingFieldname]).toBeCloseTo(0.6, 5);
});
});
34 changes: 34 additions & 0 deletions frontend/src/components/fields/Rating.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script setup lang="ts">
import { computed } from "vue";
import { Rating as FrappeRating } from "frappe-ui";

// Frappe stores Rating as a 0..1 fraction (see `_fix_rating_value` in
// frappe.model.document — values are clamped to [0, 1]). frappe-ui's Rating
// component, however, works in raw 1..N integers. Passing a 0..1 float in or
// emitting a 1..N int out silently corrupts the value. This wrapper bridges
// the two representations so the underlying fraction round-trips intact.

const MAX_STARS = 5;

defineProps<{
readonly?: boolean;
disabled?: boolean;
}>();

const modelValue = defineModel<number | null>();

const starValue = computed(() => Math.round((Number(modelValue.value) || 0) * MAX_STARS));

const onUpdate = (stars: number) => {
modelValue.value = stars / MAX_STARS;
};
</script>

<template>
<FrappeRating
:modelValue="starValue"
:rating_from="MAX_STARS"
:readonly="readonly || disabled"
@update:modelValue="onUpdate"
/>
</template>
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,12 @@ const classNames = computed<string>(() =>
editorClass="prose-sm !border-none !p-0 !shadow-none"
/>

<Rating v-else-if="fieldtype === Fieldtype.RATING" :modelValue="value" readonly />
<Rating
v-else-if="fieldtype === Fieldtype.RATING"
:modelValue="Math.round((Number(value) || 0) * 5)"
:rating_from="5"
readonly
/>

<a
v-else-if="fieldtype === Fieldtype.ATTACH && safeAttachUrl"
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/config/fieldTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import {
DateRangePicker,
DateTimePicker,
Password,
Rating,
Select,
Switch,
Textarea,
Expand Down Expand Up @@ -62,6 +61,7 @@ import Heading from "@/components/fields/Heading.vue";
import Multiselect from "@/components/fields/multiselect/Multiselect.vue";
import MultiselectBuilderExtras from "@/components/fields/multiselect/MultiselectBuilderExtras.vue";
import Phone from "@/components/fields/Phone.vue";
import Rating from "@/components/fields/Rating.vue";
import Table from "@/components/fields/Table.vue";
import { Fieldtype } from "@/types/FormsPro/form_field.types";

Expand Down
Loading