• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

cartesi / rollups-explorer / 10491533758

21 Aug 2024 02:11PM CUT coverage: 93.391% (-0.4%) from 93.766%
10491533758

Pull #232

github

nevendyulgerov
test(apps/web): Add unit tests for specifications v1 validator
Pull Request #232: #229 Add import and export for specifications

1211 of 1433 branches covered (84.51%)

Branch coverage included in aggregate %.

487 of 570 new or added lines in 9 files covered. (85.44%)

50 existing lines in 10 files now uncovered.

12863 of 13637 relevant lines covered (94.32%)

45.48 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.54
/apps/web/src/components/specification/form/fields/ByteSlices.tsx
1
import {
1✔
2
    Accordion,
1✔
3
    Button,
1✔
4
    Checkbox,
1✔
5
    Collapse,
1✔
6
    Fieldset,
1✔
7
    Group,
1✔
8
    NumberInput,
1✔
9
    Select,
1✔
10
    Stack,
1✔
11
    Switch,
1✔
12
    Table,
1✔
13
    Text,
1✔
14
    TextInput,
1✔
15
    Title,
1✔
16
    VisuallyHidden,
1✔
17
    useMantineTheme,
1✔
18
} from "@mantine/core";
1✔
19
import { createFormActions, useForm } from "@mantine/form";
1✔
20
import { useDisclosure } from "@mantine/hooks";
1✔
21
import { AbiType } from "abitype";
1✔
22
import { any, clone, gt, gte, isEmpty, isNil, lt, reject } from "ramda";
1✔
23
import {
1✔
24
    isBlank,
1✔
25
    isFunction,
1✔
26
    isNilOrEmpty,
1✔
27
    isNotNilOrEmpty,
1✔
28
} from "ramda-adjunct";
1✔
29
import { FC, ReactNode, useEffect, useRef } from "react";
1✔
30
import {
1✔
31
    TbArrowsDiagonal,
1✔
32
    TbArrowsDiagonalMinimize2,
1✔
33
    TbTrash,
1✔
34
} from "react-icons/tb";
1✔
35
import { SliceInstruction } from "../../types";
1✔
36

1✔
37
interface Props {
1✔
38
    slices: SliceInstruction[];
1✔
39
    onSliceChange: (slices: SliceInstruction[]) => void;
1✔
40
}
1✔
41

1✔
42
const InstructionsReview: FC<Props> = ({ slices, onSliceChange }) => {
1✔
43
    if (slices.length === 0) return "";
56✔
44

18✔
45
    const rows = slices.map((slice, idx) => (
18✔
46
        <Table.Tr key={slice.name} data-testid={`${idx}-${slice.name}`}>
18✔
47
            <Table.Td>{slice.name}</Table.Td>
18✔
48
            <Table.Td>{slice.from}</Table.Td>
18✔
49
            <Table.Td>{slice.to ?? "end of bytes"}</Table.Td>
18!
50
            <Table.Td>{`${isEmpty(slice.type) ? "Hex" : slice.type}`}</Table.Td>
18!
51
            <Table.Td>
18✔
52
                <Button
18✔
53
                    size="compact-sm"
18✔
54
                    color="red"
18✔
55
                    onClick={() => {
18✔
56
                        const newVal = reject(
1✔
57
                            (i: SliceInstruction) => i.name === slice.name,
1✔
58
                            slices,
1✔
59
                        );
1✔
60

1✔
61
                        onSliceChange(newVal);
1✔
62
                    }}
1✔
63
                >
18✔
64
                    <VisuallyHidden>{`Remove slice-${slice.name}`}</VisuallyHidden>
18✔
65
                    <TbTrash />
18✔
66
                </Button>
18✔
67
            </Table.Td>
18✔
68
        </Table.Tr>
18✔
69
    ));
18✔
70

18✔
71
    return (
18✔
72
        <Accordion variant="contained" chevronPosition="right" py="sm">
18✔
73
            <Accordion.Item
18✔
74
                key="byte-slice-instructions"
18✔
75
                value="byte-slice-instructions"
18✔
76
            >
18✔
77
                <Accordion.Control>
18✔
78
                    <Title order={4}>Review your definition</Title>
18✔
79
                </Accordion.Control>
18✔
80

18✔
81
                <Accordion.Panel>
18✔
82
                    <Table
18✔
83
                        horizontalSpacing="xl"
18✔
84
                        highlightOnHover
18✔
85
                        data-testid="batch-review-table"
18✔
86
                    >
18✔
87
                        <Table.Thead>
18✔
88
                            <Table.Tr>
18✔
89
                                <Table.Th style={{ whiteSpace: "nowrap" }}>
18✔
90
                                    Name
18✔
91
                                </Table.Th>
18✔
92
                                <Table.Th style={{ whiteSpace: "nowrap" }}>
18✔
93
                                    From
18✔
94
                                </Table.Th>
18✔
95
                                <Table.Th style={{ whiteSpace: "nowrap" }}>
18✔
96
                                    To
18✔
97
                                </Table.Th>
18✔
98
                                <Table.Th style={{ whiteSpace: "nowrap" }}>
18✔
99
                                    Type
18✔
100
                                </Table.Th>
18✔
101
                                <Table.Th style={{ whiteSpace: "nowrap" }}>
18✔
102
                                    Action
18✔
103
                                </Table.Th>
18✔
104
                            </Table.Tr>
18✔
105
                        </Table.Thead>
18✔
106
                        <Table.Tbody>{rows}</Table.Tbody>
18✔
107
                    </Table>
18✔
108
                </Accordion.Panel>
18✔
109
            </Accordion.Item>
18✔
110
        </Accordion>
18✔
111
    );
18✔
112
};
18✔
113

1✔
114
interface SliceInstructionFieldsProps {
1✔
115
    onSliceInstructionsChange: (slices: SliceInstruction[]) => void;
1✔
116
    onSliceTargetChange: (sliceTarget: string | undefined) => void;
1✔
117
    isActive?: boolean;
1✔
118
}
1✔
119
interface FormValues {
1✔
120
    sliceTarget: string | null;
1✔
121
    sliceTargetChecked: boolean;
1✔
122
    slices: SliceInstruction[];
1✔
123
    sliceInput: {
1✔
124
        name: string;
1✔
125
        from: string;
1✔
126
        to: string;
1✔
127
        type: string;
1✔
128
    };
1✔
129
}
1✔
130

1✔
131
const nameValidation = (value: string, values: FormValues) => {
1✔
132
    if (isBlank(value)) return "Name is required!";
110✔
133

58✔
134
    if (
58✔
135
        values.slices.length > 0 &&
58✔
136
        any((slice) => slice.name === value, values.slices)
10✔
137
    ) {
110✔
138
        return `Duplicated name. Check review`;
3✔
139
    }
3✔
140

55✔
141
    return null;
55✔
142
};
55✔
143

1✔
144
const hasOverlap = (value: number, slices: SliceInstruction[]) =>
1✔
145
    any((slice) => {
4✔
146
        if (isNilOrEmpty(slice.to)) {
4!
147
            return gte(value, slice.from);
×
148
        }
×
149

4✔
150
        return gt(value, slice.from) && lt(value, slice.to!);
4✔
151
    }, slices);
4✔
152

1✔
153
const fromValidation = (value: string, values: FormValues) => {
1✔
154
    if (isBlank(value)) return "From is required!";
110✔
155

37✔
156
    if (isNotNilOrEmpty(values.sliceInput.to) && value > values.sliceInput.to)
110✔
157
        return "From can't be bigger than To value.";
110!
158

37✔
159
    const from = parseInt(value);
37✔
160

37✔
161
    if (values.slices.length > 0 && hasOverlap(from, values.slices)) {
110✔
162
        return "Overlap with added entry! Check review.";
4✔
163
    }
4✔
164
    return null;
33✔
165
};
33✔
166

1✔
167
const toValidation = (value: string, values: FormValues) => {
1✔
168
    if (isBlank(value)) return null;
110✔
169

15✔
170
    if (
15✔
171
        isNotNilOrEmpty(values.sliceInput.from) &&
15✔
172
        value < values.sliceInput.from
15✔
173
    )
110✔
174
        return "To value can't be smaller than From field.";
110!
175

15✔
176
    const to = parseInt(value);
15✔
177

15✔
178
    if (values.slices.length > 0 && hasOverlap(to, values.slices)) {
110!
179
        return "Overlap with added entry! Check review.";
×
180
    }
✔
181
    return null;
15✔
182
};
15✔
183

1✔
184
const initialValues: FormValues = {
1✔
185
    sliceTarget: null,
1✔
186
    sliceTargetChecked: false,
1✔
187
    slices: [] as SliceInstruction[],
1✔
188
    sliceInput: {
1✔
189
        name: "",
1✔
190
        from: "",
1✔
191
        to: "",
1✔
192
        type: "",
1✔
193
    },
1✔
194
};
1✔
195

1✔
196
export const byteSlicesFormActions =
1✔
197
    createFormActions<FormValues>("byte-slices-form");
1✔
198

1✔
199
const SliceInstructionFields: FC<SliceInstructionFieldsProps> = ({
1✔
200
    onSliceInstructionsChange,
83✔
201
    onSliceTargetChange,
83✔
202
}) => {
83✔
203
    const theme = useMantineTheme();
83✔
204
    const [expanded, { toggle }] = useDisclosure(true);
83✔
205
    const sliceNameRef = useRef<HTMLInputElement>(null);
83✔
206
    const form = useForm<FormValues>({
83✔
207
        name: "byte-slices-form",
83✔
208
        initialValues: clone(initialValues),
83✔
209
        validateInputOnChange: true,
83✔
210
        validate: {
83✔
211
            sliceInput: {
83✔
212
                name: nameValidation,
83✔
213
                from: fromValidation,
83✔
214
                to: toValidation,
83✔
215
            },
83✔
216
        },
83✔
217
    });
83✔
218

83✔
219
    const { slices, sliceInput, sliceTarget, sliceTargetChecked } =
83✔
220
        form.getTransformedValues();
83✔
221

83✔
222
    const sliceNames = slices.map((slice, idx) => slice.name ?? `slice-${idx}`);
83!
223
    const key = JSON.stringify(slices);
83✔
224

83✔
225
    useEffect(() => {
83✔
226
        if (isFunction(onSliceInstructionsChange))
13✔
227
            onSliceInstructionsChange(clone(slices));
13✔
228
        // eslint-disable-next-line react-hooks/exhaustive-deps
13✔
229
    }, [key]);
83✔
230

83✔
231
    return (
83✔
232
        <Stack data-testid="slice-instruction-fields">
83✔
233
            <Fieldset p="xs">
83✔
234
                <Group justify="space-between">
83✔
235
                    <Text fw="bold">Define a slice</Text>
83✔
236
                    <Button
83✔
237
                        onClick={toggle}
83✔
238
                        size="compact-sm"
83✔
239
                        px="sm"
83✔
240
                        data-testid="byte-slice-form-resize"
83✔
241
                    >
83✔
242
                        {expanded ? (
83✔
243
                            <TbArrowsDiagonalMinimize2
83✔
244
                                size={theme?.other.iconSize ?? 21}
83!
245
                            />
83!
246
                        ) : (
×
247
                            <TbArrowsDiagonal
×
248
                                size={theme?.other.iconSize ?? 21}
×
249
                            />
×
250
                        )}
83✔
251
                    </Button>
83✔
252
                </Group>
83✔
253
                <Collapse
83✔
254
                    in={expanded}
83✔
255
                    onTransitionEnd={() => sliceNameRef.current?.focus()}
83✔
256
                >
83✔
257
                    <Stack mt="sm">
83✔
258
                        <TextInput
83✔
259
                            autoFocus
83✔
260
                            ref={sliceNameRef}
83✔
261
                            data-testid="slice-name-input"
83✔
262
                            label="Name"
83✔
263
                            description="Name to set the decoded value in the final object result."
83✔
264
                            placeholder="e.g. amount"
83✔
265
                            withAsterisk
83✔
266
                            {...form.getInputProps("sliceInput.name")}
83✔
267
                            error={form.getInputProps("sliceInput.name").error}
83✔
268
                        />
83✔
269
                        <NumberInput
83✔
270
                            allowNegative={false}
83✔
271
                            allowDecimal={false}
83✔
272
                            hideControls
83✔
273
                            data-testid="slice-from-input"
83✔
274
                            label="From"
83✔
275
                            description="Point to start the reading"
83✔
276
                            withAsterisk
83✔
277
                            placeholder="e.g. 0"
83✔
278
                            {...form.getInputProps("sliceInput.from")}
83✔
279
                        />
83✔
280
                        <NumberInput
83✔
281
                            allowNegative={false}
83✔
282
                            allowDecimal={false}
83✔
283
                            hideControls
83✔
284
                            data-testid="slice-to-input"
83✔
285
                            label="To"
83✔
286
                            description="Optional: Empty means from defined number until the end of the bytes."
83✔
287
                            placeholder="e.g. 20"
83✔
288
                            {...form.getInputProps("sliceInput.to")}
83✔
289
                        />
83✔
290
                        <TextInput
83✔
291
                            label="Type"
83✔
292
                            data-testid="slice-type-input"
83✔
293
                            placeholder="e.g. uint"
83✔
294
                            description="Optional: Empty means return slice value as-is (i.e. Hex.)"
83✔
295
                            {...form.getInputProps("sliceInput.type")}
83✔
296
                        />
83✔
297
                    </Stack>
83✔
298
                    <Group justify="flex-end" mt="md">
83✔
299
                        <Button
83✔
300
                            size="sm"
83✔
301
                            disabled={!form.isValid("sliceInput")}
83✔
302
                            data-testid="slice-add-button"
83✔
303
                            onClick={() => {
83✔
304
                                const name = sliceInput.name;
5✔
305
                                const type = sliceInput.type as AbiType;
5✔
306
                                const from = parseInt(sliceInput?.from ?? "0");
5!
307
                                const to =
5✔
308
                                    !isNil(sliceInput?.to) &&
5✔
309
                                    !isEmpty(sliceInput.to)
5✔
310
                                        ? parseInt(sliceInput?.to)
5!
311
                                        : undefined;
×
312

5✔
313
                                const slice: SliceInstruction = {
5✔
314
                                    from,
5✔
315
                                    to,
5✔
316
                                    name,
5✔
317
                                    type,
5✔
318
                                };
5✔
319

5✔
320
                                const newSlices = [...(slices ?? []), slice];
5!
321

5✔
322
                                form.setValues({
5✔
323
                                    sliceInput: clone(initialValues.sliceInput),
5✔
324
                                    slices: newSlices,
5✔
325
                                });
5✔
326
                                sliceNameRef.current?.focus();
5✔
327
                            }}
5✔
328
                        >
83✔
329
                            Add
83✔
330
                        </Button>
83✔
331
                    </Group>
83✔
332
                </Collapse>
83✔
333
            </Fieldset>
83✔
334

83✔
335
            <InstructionsReview
83✔
336
                slices={slices}
83✔
337
                onSliceChange={(slices) => {
83✔
338
                    form.setFieldValue("slices", slices);
1✔
339
                }}
1✔
340
            />
83✔
341

83✔
342
            {sliceNames && sliceNames.length > 0 ? (
83✔
343
                <Group>
25✔
344
                    <Checkbox
25✔
345
                        data-testid="slice-apply-abi-on-checkbox"
25✔
346
                        checked={sliceTargetChecked}
25✔
347
                        onChange={() => {
25✔
348
                            form.setFieldValue(
2✔
349
                                "sliceTargetChecked",
2✔
350
                                !sliceTargetChecked,
2✔
351
                            );
2✔
352
                            if (sliceTarget !== null) {
2!
UNCOV
353
                                form.setFieldValue("sliceTarget", null);
×
354
                                if (isFunction(onSliceTargetChange))
×
355
                                    onSliceTargetChange(undefined);
×
356
                            }
×
357
                        }}
2✔
358
                        label="Use ABI Parameter definition on"
25✔
359
                    />
25✔
360
                    <Select
25✔
361
                        name="slice"
25✔
362
                        data-testid="slice-select"
25✔
363
                        key={sliceNames.join(",")}
25✔
364
                        placeholder="Pick a slice"
25✔
365
                        data={sliceNames}
25✔
366
                        disabled={!sliceTargetChecked}
25✔
367
                        value={sliceTarget}
25✔
368
                        onChange={(value) => {
25✔
369
                            form.setFieldValue("sliceTarget", value);
2✔
370
                            if (isFunction(onSliceTargetChange))
2✔
371
                                onSliceTargetChange(value ?? undefined);
2!
372
                        }}
2✔
373
                    />
25✔
374
                </Group>
25✔
375
            ) : (
58✔
376
                ""
58✔
377
            )}
83✔
378
        </Stack>
83✔
379
    );
83✔
380
};
83✔
381

1✔
382
interface ByteSlicesProps extends SliceInstructionFieldsProps {
1✔
383
    onSwitchChange: (active: boolean) => void;
1✔
384
    error?: string | ReactNode;
1✔
385
    isActive: boolean;
1✔
386
}
1✔
387

1✔
388
export const ByteSlices: FC<ByteSlicesProps> = ({
1✔
389
    onSliceInstructionsChange,
43✔
390
    onSliceTargetChange,
43✔
391
    onSwitchChange,
43✔
392
    error,
43✔
393
    isActive,
43✔
394
}) => {
43✔
395
    return (
43✔
396
        <Stack>
43✔
397
            <Group justify="space-between" align="normal" wrap="nowrap">
43✔
398
                <Stack gap={0}>
43✔
399
                    <Text component="span" fw="bold">
43✔
400
                        Add decoding steps by byte range
43✔
401
                    </Text>
43✔
402
                    <Text size="xs" c="dimmed" component="span">
43✔
403
                        Helpful when using non-standard abi-encoding.
43✔
404
                    </Text>
43✔
405
                    {error && (
43✔
406
                        <Text c="red" size="xs">
2✔
407
                            {error}
2✔
408
                        </Text>
2✔
409
                    )}
43✔
410
                </Stack>
43✔
411
                <Switch
43✔
412
                    checked={isActive}
43✔
413
                    onClick={(evt) => {
43✔
414
                        const val = evt.currentTarget.checked;
7✔
415
                        onSwitchChange(val);
7✔
416
                        if (!val) {
7!
UNCOV
417
                            onSliceInstructionsChange([]);
×
418
                            onSliceTargetChange(undefined);
×
419
                        }
×
420
                    }}
7✔
421
                    data-testid="add-byte-slice-switch"
43✔
422
                />
43✔
423
            </Group>
43✔
424

43✔
425
            {isActive ? (
43✔
426
                <Stack pl="sm">
20✔
427
                    <SliceInstructionFields
20✔
428
                        onSliceInstructionsChange={onSliceInstructionsChange}
20✔
429
                        onSliceTargetChange={onSliceTargetChange}
20✔
430
                    />
20✔
431
                </Stack>
20✔
432
            ) : (
23✔
433
                ""
23✔
434
            )}
43✔
435
        </Stack>
43✔
436
    );
43✔
437
};
43✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc