Skip to content

Commit c84b521

Browse files
authored
Merge branch 'main' into lkostrowski/model-type-icons
2 parents 13bd3b9 + 23f3b88 commit c84b521

6 files changed

Lines changed: 182 additions & 2 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"saleor-dashboard": patch
3+
---
4+
5+
Discount rule reward values now keep their decimal part.
6+
7+
Typing `12.55` into a rule's reward value stored `12` — the field parsed input with `parseInt`, so everything after the decimal point was dropped without any feedback. Fractional rewards below `1`, such as a `0.5%` discount, were also rejected with "Rule reward value is required". Both now work, for fixed-amount and percentage rewards alike.

.github/workflows/pr-automation.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,8 @@ jobs:
143143
IS_CLOUD_INSTANCE: true
144144
ENABLED_SERVICE_NAME_HEADER: true
145145
ONBOARDING_USER_JOINED_DATE_THRESHOLD: ${{ vars.STAGING_ONBOARDING_USER_JOINED_DATE_THRESHOLD }}
146+
# PR test envs run core unstable/unreleased main, so build against the staging schema
147+
FF_USE_STAGING_SCHEMA: true
146148
run: pnpm run build
147149

148150
- name: Upload Build
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { type Rule } from "@dashboard/discounts/models";
2+
import { PromotionTypeEnum, RewardValueTypeEnum } from "@dashboard/graphql";
3+
import { ThemeProvider } from "@saleor/macaw-ui-next";
4+
import { render, screen } from "@testing-library/react";
5+
import userEvent from "@testing-library/user-event";
6+
import { type ReactNode } from "react";
7+
import { FormProvider, useForm } from "react-hook-form";
8+
9+
import { DiscountRulesContextProvider } from "../../../../context";
10+
import { RuleRewardValue } from "./RuleRewardValue";
11+
12+
const defaultValues = {
13+
rewardValue: null,
14+
rewardValueType: RewardValueTypeEnum.FIXED,
15+
} as unknown as Rule;
16+
17+
let submittedValue: number | null | undefined;
18+
19+
const Wrapper = ({ children }: { children: ReactNode }) => {
20+
const methods = useForm<Rule>({ defaultValues });
21+
22+
submittedValue = methods.watch("rewardValue");
23+
24+
return (
25+
<ThemeProvider>
26+
<DiscountRulesContextProvider
27+
discountType={PromotionTypeEnum.CATALOGUE}
28+
channels={[]}
29+
disabled={false}
30+
>
31+
<FormProvider {...methods}>{children}</FormProvider>
32+
</DiscountRulesContextProvider>
33+
</ThemeProvider>
34+
);
35+
};
36+
37+
const renderRewardValue = () =>
38+
render(<RuleRewardValue currencySymbol="USD" error={undefined} />, { wrapper: Wrapper });
39+
40+
describe("RuleRewardValue", () => {
41+
beforeEach(() => {
42+
submittedValue = undefined;
43+
});
44+
45+
it("keeps the fractional part of a decimal reward value", async () => {
46+
// Arrange
47+
renderRewardValue();
48+
49+
const input = screen.getByTestId("reward-value-input");
50+
51+
// Act
52+
await userEvent.type(input, "12.55");
53+
54+
// Assert
55+
expect(submittedValue).toBe(12.55);
56+
});
57+
58+
it("keeps a reward value below one", async () => {
59+
// Arrange
60+
renderRewardValue();
61+
62+
const input = screen.getByTestId("reward-value-input");
63+
64+
// Act
65+
await userEvent.type(input, "0.5");
66+
67+
// Assert
68+
expect(submittedValue).toBe(0.5);
69+
});
70+
71+
it("clears the reward value when the input is emptied", async () => {
72+
// Arrange
73+
renderRewardValue();
74+
75+
const input = screen.getByTestId("reward-value-input");
76+
77+
await userEvent.type(input, "12");
78+
79+
// Act
80+
await userEvent.clear(input);
81+
82+
// Assert
83+
expect(submittedValue).toBeNull();
84+
});
85+
86+
it("allows decimals to be entered", () => {
87+
// Arrange & Act
88+
renderRewardValue();
89+
90+
// Assert
91+
expect(screen.getByTestId("reward-value-input")).toHaveAttribute("step", "any");
92+
});
93+
});

src/discounts/components/DiscountRules/componenets/RuleForm/components/RuleRewardValue/RuleRewardValue.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,14 @@ export const RuleRewardValue = ({ currencySymbol, error }: RuleRewardPriceProps)
4343
name={rewardValueType.name}
4444
ref={rewardValueType.ref}
4545
onChange={e => {
46-
const value = parseInt(e.target.value, 10);
46+
const value = parseFloat(e.target.value);
4747

4848
rewardValueType.onChange(Number.isNaN(value) ? null : value);
4949
}}
5050
error={!!rewardValueError}
5151
disabled={disabled || rewardValueType.disabled}
5252
type="number"
53+
step="any"
5354
size="small"
5455
label={intl.formatMessage(messages.discountValue)}
5556
data-test-id="reward-value-input"
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { RewardValueTypeEnum } from "@dashboard/graphql";
2+
import { testIntlInstance } from "@test/intl";
3+
4+
import { getValidationSchema } from "./validationSchema";
5+
6+
const buildRule = (overrides: Record<string, unknown> = {}) => ({
7+
name: "Rule name",
8+
description: null,
9+
channel: { label: "Default channel", value: "channel-id" },
10+
conditions: [],
11+
rewardType: null,
12+
rewardValueType: RewardValueTypeEnum.FIXED,
13+
rewardValue: 10,
14+
...overrides,
15+
});
16+
17+
const getRewardValueError = (rule: ReturnType<typeof buildRule>) => {
18+
const result = getValidationSchema(testIntlInstance).safeParse(rule);
19+
20+
if (result.success) {
21+
return undefined;
22+
}
23+
24+
return result.error.issues.find(issue => issue.path.includes("rewardValue"));
25+
};
26+
27+
describe("getValidationSchema", () => {
28+
it.each([12.55, 0.5, 0.01])("accepts %p as a reward value", rewardValue => {
29+
// Arrange
30+
const rule = buildRule({ rewardValue });
31+
32+
// Act
33+
const error = getRewardValueError(rule);
34+
35+
// Assert
36+
expect(error).toBeUndefined();
37+
});
38+
39+
it.each([0, -1, null])("rejects %p as a reward value", rewardValue => {
40+
// Arrange
41+
const rule = buildRule({ rewardValue });
42+
43+
// Act
44+
const error = getRewardValueError(rule);
45+
46+
// Assert
47+
expect(error).toBeDefined();
48+
});
49+
50+
it("rejects a percentage reward value above 100", () => {
51+
// Arrange
52+
const rule = buildRule({
53+
rewardValueType: RewardValueTypeEnum.PERCENTAGE,
54+
rewardValue: 100.5,
55+
});
56+
57+
// Act
58+
const error = getRewardValueError(rule);
59+
60+
// Assert
61+
expect(error).toBeDefined();
62+
});
63+
64+
it("accepts a fractional percentage reward value", () => {
65+
// Arrange
66+
const rule = buildRule({
67+
rewardValueType: RewardValueTypeEnum.PERCENTAGE,
68+
rewardValue: 12.5,
69+
});
70+
71+
// Act
72+
const error = getRewardValueError(rule);
73+
74+
// Assert
75+
expect(error).toBeUndefined();
76+
});
77+
});

src/discounts/components/DiscountRules/componenets/RuleFormModal/validationSchema.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const rewardValueRequired = (intl: IntlShape) =>
3131
required_error: intl.formatMessage(validationMessages.rewardValueRequired),
3232
invalid_type_error: intl.formatMessage(validationMessages.rewardValueRequired),
3333
})
34-
.min(1, intl.formatMessage(validationMessages.rewardValueRequired));
34+
.gt(0, intl.formatMessage(validationMessages.rewardValueRequired));
3535
const getDefaultSchema = (intl: IntlShape) =>
3636
z.object({
3737
id: z.string().optional(),

0 commit comments

Comments
 (0)