@griddo/ax 12.10.5 → 12.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/__tests__/components/Fields/ConditionalField/ConditionalField.test.tsx +17 -0
- package/src/__tests__/components/Fields/RadioField/RadioField.test.tsx +66 -0
- package/src/__tests__/components/Fields/RadioGroup/RadioGroup.test.tsx +76 -0
- package/src/__tests__/modules/Redirects/BulkHeader/TableHeader.test.tsx +123 -0
- package/src/__tests__/modules/Redirects/RedirectItem/RedirectItem.test.tsx +163 -0
- package/src/__tests__/modules/Redirects/RedirectPanel/RedirectPanel.test.tsx +134 -0
- package/src/__tests__/modules/Redirects/Redirects/Redirects.create.test.tsx +69 -0
- package/src/__tests__/modules/Redirects/Redirects/Redirects.edit.test.tsx +91 -0
- package/src/__tests__/modules/Redirects/Redirects/Redirects.filter.test.tsx +131 -0
- package/src/components/Fields/ConditionalField/style.tsx +1 -1
- package/src/components/Fields/RadioField/index.tsx +20 -3
- package/src/components/Fields/RadioField/style.tsx +18 -1
- package/src/components/Fields/RadioGroup/index.tsx +1 -0
- package/src/containers/Redirects/actions.tsx +15 -3
- package/src/modules/Redirects/BulkHeader/TableHeader/index.tsx +5 -0
- package/src/modules/Redirects/BulkHeader/TableHeader/style.tsx +5 -1
- package/src/modules/Redirects/RedirectItem/index.tsx +11 -1
- package/src/modules/Redirects/RedirectItem/style.tsx +29 -1
- package/src/modules/Redirects/RedirectPanel/index.tsx +40 -3
- package/src/modules/Redirects/TypeFilter/index.tsx +44 -0
- package/src/modules/Redirects/TypeFilter/style.tsx +27 -0
- package/src/modules/Redirects/hooks.tsx +12 -4
- package/src/modules/Redirects/index.tsx +22 -14
- package/src/modules/Redirects/utils.tsx +10 -1
- package/src/types/index.tsx +3 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@griddo/ax",
|
|
3
3
|
"description": "Griddo Author Experience",
|
|
4
|
-
"version": "12.
|
|
4
|
+
"version": "12.11.0",
|
|
5
5
|
"authors": [
|
|
6
6
|
"Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
|
|
7
7
|
"Diego M. Béjar <diego.bejar@secuoyas.com>",
|
|
@@ -201,5 +201,5 @@
|
|
|
201
201
|
"publishConfig": {
|
|
202
202
|
"access": "public"
|
|
203
203
|
},
|
|
204
|
-
"gitHead": "
|
|
204
|
+
"gitHead": "84947272b2aeecc017658fb0463be48c44895bb1"
|
|
205
205
|
}
|
|
@@ -87,6 +87,23 @@ describe("ConditionalField component rendering", () => {
|
|
|
87
87
|
});
|
|
88
88
|
});
|
|
89
89
|
|
|
90
|
+
describe("ConditionalField spacing", () => {
|
|
91
|
+
it("should reset the bottom margin of its direct child only", () => {
|
|
92
|
+
render(
|
|
93
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
94
|
+
<ConditionalField {...defaultProps} />
|
|
95
|
+
</ThemeProvider>,
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const conditionalFieldContent = screen.getByTestId("conditionalFieldContent");
|
|
99
|
+
|
|
100
|
+
expect(conditionalFieldContent).toHaveStyleRule("margin-bottom", "0", { modifier: "> div:last-child" });
|
|
101
|
+
// A descendant selector would also reach every nested field and "Add" button,
|
|
102
|
+
// flattening the spacing of nested ArrayFieldGroups (sc-110266).
|
|
103
|
+
expect(conditionalFieldContent).not.toHaveStyleRule("margin-bottom", "0", { modifier: "div:last-child" });
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
90
107
|
interface IConditionalFieldProps {
|
|
91
108
|
value: string | boolean;
|
|
92
109
|
options: any[];
|
|
@@ -94,3 +94,69 @@ describe("onClick events", () => {
|
|
|
94
94
|
expect(defaultProps.onChange).toHaveBeenCalledTimes(1);
|
|
95
95
|
});
|
|
96
96
|
});
|
|
97
|
+
|
|
98
|
+
describe("RadioField description", () => {
|
|
99
|
+
const baseProps: IRadioFieldProps = {
|
|
100
|
+
name: "redirectionType",
|
|
101
|
+
value: "301",
|
|
102
|
+
title: "301 - Permanent",
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const renderField = (props: IRadioFieldProps) =>
|
|
106
|
+
render(
|
|
107
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
108
|
+
<RadioField {...props} />
|
|
109
|
+
</ThemeProvider>,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
it("should not render a description when none is given", () => {
|
|
113
|
+
renderField(baseProps);
|
|
114
|
+
|
|
115
|
+
expect(screen.queryByTestId("radio-field-description")).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("should render the description below the title", () => {
|
|
119
|
+
renderField({ ...baseProps, description: "Keeps SEO. Use for definitive redirects." });
|
|
120
|
+
|
|
121
|
+
const description = screen.getByTestId("radio-field-description");
|
|
122
|
+
expect(description.textContent).toEqual("Keeps SEO. Use for definitive redirects.");
|
|
123
|
+
expect(description).toHaveStyleRule("display", "block");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("should render the description smaller than the title", () => {
|
|
127
|
+
// Mismo tamaño que el HelpText de FieldsBehavior (uiXS), que es el texto
|
|
128
|
+
// descriptivo que ya usa el CMS bajo un campo.
|
|
129
|
+
renderField({ ...baseProps, description: "Keeps SEO." });
|
|
130
|
+
|
|
131
|
+
expect(screen.getByTestId("radio-field-description")).toHaveStyleRule("font-size", "12px");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should name the radio with the title alone", () => {
|
|
135
|
+
// Con la descripción colgando del <label> sin más, el nombre accesible
|
|
136
|
+
// concatenaba las dos cadenas sin separador ("301 - PermanentKeeps SEO.").
|
|
137
|
+
renderField({ ...baseProps, description: "Keeps SEO. Use for definitive redirects." });
|
|
138
|
+
|
|
139
|
+
const radio = screen.getByRole("radio", { name: "301 - Permanent" });
|
|
140
|
+
expect(radio).toBeTruthy();
|
|
141
|
+
expect(radio.getAttribute("aria-describedby")).toEqual("redirectionType-301-description");
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("should emphasise the title only when there is a description", () => {
|
|
145
|
+
const { container: withDescription } = renderField({ ...baseProps, description: "Keeps SEO." });
|
|
146
|
+
expect(withDescription.querySelector("label")).toHaveStyleRule("font-weight", "600");
|
|
147
|
+
|
|
148
|
+
cleanup();
|
|
149
|
+
|
|
150
|
+
const { container: withoutDescription } = renderField(baseProps);
|
|
151
|
+
expect(withoutDescription.querySelector("label")).not.toHaveStyleRule("font-weight", "600");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should select the option when the description is clicked", () => {
|
|
155
|
+
const onChange = vi.fn();
|
|
156
|
+
renderField({ ...baseProps, description: "Keeps SEO.", onChange });
|
|
157
|
+
|
|
158
|
+
fireEvent.click(screen.getByTestId("radio-field-description"));
|
|
159
|
+
|
|
160
|
+
expect(onChange).toHaveBeenCalledWith("301");
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
2
|
+
|
|
3
|
+
import RadioGroup from "@ax/components/Fields/RadioGroup";
|
|
4
|
+
import { parseTheme } from "@ax/helpers";
|
|
5
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
6
|
+
|
|
7
|
+
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
|
8
|
+
import { ThemeProvider } from "styled-components";
|
|
9
|
+
|
|
10
|
+
afterEach(cleanup);
|
|
11
|
+
|
|
12
|
+
const options = [
|
|
13
|
+
{
|
|
14
|
+
name: "301",
|
|
15
|
+
value: "301",
|
|
16
|
+
title: "301 - Permanent",
|
|
17
|
+
description: "Keeps SEO. Use for definitive redirects.",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
name: "302",
|
|
21
|
+
value: "302",
|
|
22
|
+
title: "302 - Temporary",
|
|
23
|
+
description: "Doesn't keep SEO. Use for short-lived changes.",
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const renderGroup = (props: Partial<React.ComponentProps<typeof RadioGroup>> = {}) =>
|
|
28
|
+
render(
|
|
29
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
30
|
+
<RadioGroup name="redirectionType" value="301" options={options} onChange={vi.fn()} {...props} />
|
|
31
|
+
</ThemeProvider>,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
describe("RadioGroup component rendering", () => {
|
|
35
|
+
it("should render one radio per option", () => {
|
|
36
|
+
renderGroup();
|
|
37
|
+
|
|
38
|
+
expect(screen.getAllByTestId("radio-field-input")).toHaveLength(2);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("should check the option matching the value", () => {
|
|
42
|
+
renderGroup();
|
|
43
|
+
|
|
44
|
+
const [permanent, temporary] = screen.getAllByTestId<HTMLInputElement>("radio-field-input");
|
|
45
|
+
expect(permanent.checked).toBe(true);
|
|
46
|
+
expect(temporary.checked).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("should forward each option description to its radio", () => {
|
|
50
|
+
renderGroup();
|
|
51
|
+
|
|
52
|
+
const descriptions = screen.getAllByTestId("radio-field-description").map((node) => node.textContent);
|
|
53
|
+
expect(descriptions).toEqual([
|
|
54
|
+
"Keeps SEO. Use for definitive redirects.",
|
|
55
|
+
"Doesn't keep SEO. Use for short-lived changes.",
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should render no descriptions when the options have none", () => {
|
|
60
|
+
renderGroup({ options: options.map(({ description: _description, ...option }) => option) });
|
|
61
|
+
|
|
62
|
+
expect(screen.queryByTestId("radio-field-description")).toBeNull();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("RadioGroup events", () => {
|
|
67
|
+
it("should report the value of the clicked option", () => {
|
|
68
|
+
const onChange = vi.fn();
|
|
69
|
+
renderGroup({ onChange });
|
|
70
|
+
|
|
71
|
+
const [, temporary] = screen.getAllByTestId("radio-field-input");
|
|
72
|
+
fireEvent.click(temporary);
|
|
73
|
+
|
|
74
|
+
expect(onChange).toHaveBeenCalledWith("302");
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { parseTheme } from "@ax/helpers";
|
|
2
|
+
import TableHeader from "@ax/modules/Redirects/BulkHeader/TableHeader";
|
|
3
|
+
import * as HeaderStyle from "@ax/modules/Redirects/BulkHeader/TableHeader/style";
|
|
4
|
+
import * as ItemStyle from "@ax/modules/Redirects/RedirectItem/style";
|
|
5
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
6
|
+
import type { IQueryValue } from "@ax/types";
|
|
7
|
+
|
|
8
|
+
import configureStore from "redux-mock-store";
|
|
9
|
+
import thunk from "redux-thunk";
|
|
10
|
+
import { ThemeProvider } from "styled-components";
|
|
11
|
+
|
|
12
|
+
import { cleanup, fireEvent, render, screen } from "../../../../../config/tests/test-utils";
|
|
13
|
+
|
|
14
|
+
afterEach(cleanup);
|
|
15
|
+
|
|
16
|
+
const store = configureStore([thunk])({
|
|
17
|
+
app: { isSaving: false, lang: { locale: "en_EN", id: 1 }, globalLangs: [] },
|
|
18
|
+
sites: { currentSiteInfo: null, sites: [{ id: 1, name: "UST School" }] },
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const renderHeader = (
|
|
22
|
+
isSiteItem: boolean,
|
|
23
|
+
filterItems: (filterPointer: string, filtersSelected: IQueryValue[]) => void = vi.fn(),
|
|
24
|
+
type: IQueryValue[] = [],
|
|
25
|
+
) =>
|
|
26
|
+
render(
|
|
27
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
28
|
+
<TableHeader
|
|
29
|
+
totalItems={1}
|
|
30
|
+
isScrolling={false}
|
|
31
|
+
selectAllItems={vi.fn()}
|
|
32
|
+
filterItems={filterItems}
|
|
33
|
+
filterValues={{ sites: [], type, order: [] }}
|
|
34
|
+
isSiteItem={isSiteItem}
|
|
35
|
+
sortItems={vi.fn()}
|
|
36
|
+
sortedListStatus={{ isAscending: false, sortedByDate: false }}
|
|
37
|
+
checkState={{ isAllSelected: false, hoverCheck: false }}
|
|
38
|
+
setHoverCheck={vi.fn()}
|
|
39
|
+
/>
|
|
40
|
+
</ThemeProvider>,
|
|
41
|
+
{ store },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
describe("[UC-AX-manage-redirects] Redirects TableHeader", () => {
|
|
45
|
+
it("AC8 · heads the redirection type column in global scope", () => {
|
|
46
|
+
renderHeader(false);
|
|
47
|
+
|
|
48
|
+
expect(screen.getByText("Type")).toBeTruthy();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("AC8 · heads it in site scope too", () => {
|
|
52
|
+
renderHeader(true);
|
|
53
|
+
|
|
54
|
+
expect(screen.getByText("Type")).toBeTruthy();
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("[UC-AX-manage-redirects] Redirects type column alignment", () => {
|
|
59
|
+
// La tabla es flex con anchuras fijas por celda: cabecera y fila se alinean
|
|
60
|
+
// solo porque coinciden a mano. Si una de las dos deriva, la columna entera
|
|
61
|
+
// se descuadra y jsdom no hace layout, así que nadie más lo vería.
|
|
62
|
+
const width = "96px";
|
|
63
|
+
|
|
64
|
+
it("AC8 · keeps header and cell the same width", () => {
|
|
65
|
+
const { container } = render(
|
|
66
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
67
|
+
<>
|
|
68
|
+
<HeaderStyle.TypeHeader data-testid="header" />
|
|
69
|
+
<ItemStyle.TypeCell data-testid="cell" />
|
|
70
|
+
</>
|
|
71
|
+
</ThemeProvider>,
|
|
72
|
+
{ store },
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
expect(container.querySelector('[data-testid="header"]')).toHaveStyleRule("width", width);
|
|
76
|
+
expect(container.querySelector('[data-testid="cell"]')).toHaveStyleRule("width", width);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("[UC-AX-manage-redirects] Redirects type filter", () => {
|
|
81
|
+
const openTypeFilter = () => fireEvent.click(screen.getByTestId("redirect-type-filter"));
|
|
82
|
+
|
|
83
|
+
const optionFor = (label: string) => screen.getAllByTestId("list-item").find((item) => item.textContent === label);
|
|
84
|
+
|
|
85
|
+
it("AC9 · filters the listing by the chosen type", () => {
|
|
86
|
+
const filterItems = vi.fn();
|
|
87
|
+
renderHeader(false, filterItems);
|
|
88
|
+
|
|
89
|
+
openTypeFilter();
|
|
90
|
+
fireEvent.click(optionFor("302") as HTMLElement);
|
|
91
|
+
|
|
92
|
+
expect(filterItems).toHaveBeenCalledWith("type", [{ value: "302", label: "302" }]);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("AC9 · offers 301 and 302, with none chosen to start with", () => {
|
|
96
|
+
renderHeader(false);
|
|
97
|
+
|
|
98
|
+
openTypeFilter();
|
|
99
|
+
|
|
100
|
+
expect(screen.getByText("Filter by type")).toBeTruthy();
|
|
101
|
+
expect(screen.getAllByTestId("list-item").map((item) => item.textContent)).toEqual(["301", "302"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("AC9 · clears the filter when the chosen type is picked again", () => {
|
|
105
|
+
const filterItems = vi.fn();
|
|
106
|
+
renderHeader(false, filterItems, [{ value: "302", label: "302" }]);
|
|
107
|
+
|
|
108
|
+
openTypeFilter();
|
|
109
|
+
fireEvent.click(optionFor("302") as HTMLElement);
|
|
110
|
+
|
|
111
|
+
expect(filterItems).toHaveBeenCalledWith("type", []);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("AC9 · offers the filter in site scope too, where the Site one is hidden", () => {
|
|
115
|
+
const filterItems = vi.fn();
|
|
116
|
+
renderHeader(true, filterItems);
|
|
117
|
+
|
|
118
|
+
openTypeFilter();
|
|
119
|
+
fireEvent.click(optionFor("301") as HTMLElement);
|
|
120
|
+
|
|
121
|
+
expect(filterItems).toHaveBeenCalledWith("type", [{ value: "301", label: "301" }]);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { parseTheme } from "@ax/helpers";
|
|
2
|
+
import RedirectItem from "@ax/modules/Redirects/RedirectItem";
|
|
3
|
+
import * as S from "@ax/modules/Redirects/RedirectItem/style";
|
|
4
|
+
import { REDIRECT_TYPE_COLORS } from "@ax/modules/Redirects/utils";
|
|
5
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
6
|
+
import type { IRedirect, RedirectType } from "@ax/types";
|
|
7
|
+
|
|
8
|
+
import configureStore from "redux-mock-store";
|
|
9
|
+
import thunk from "redux-thunk";
|
|
10
|
+
import { ThemeProvider } from "styled-components";
|
|
11
|
+
|
|
12
|
+
import { cleanup, fireEvent, render, screen } from "../../../../../config/tests/test-utils";
|
|
13
|
+
|
|
14
|
+
afterEach(cleanup);
|
|
15
|
+
|
|
16
|
+
const mockStore = configureStore([thunk]);
|
|
17
|
+
const store = mockStore({
|
|
18
|
+
app: { isSaving: false, lang: { locale: "en_EN", id: 1 }, globalLangs: [] },
|
|
19
|
+
sites: { currentSiteInfo: null, sites: [{ id: 1, name: "UST School" }] },
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const redirect: IRedirect = {
|
|
23
|
+
id: 1,
|
|
24
|
+
from: "/about/team",
|
|
25
|
+
to: { pageId: 7, url: "/about/people" },
|
|
26
|
+
site: { siteId: 2, siteName: "UST School", siteUrl: "/ust" },
|
|
27
|
+
domain: "https://ust.edu",
|
|
28
|
+
date: "2021-10-15T00:00:00.000Z",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const renderItem = (overrides: Partial<IRedirect> = {}, isSiteItem = false, setFormValues = vi.fn()) =>
|
|
32
|
+
render(
|
|
33
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
34
|
+
<RedirectItem
|
|
35
|
+
redirect={{ ...redirect, ...overrides }}
|
|
36
|
+
isSelected={false}
|
|
37
|
+
onChange={vi.fn()}
|
|
38
|
+
toggleToast={vi.fn()}
|
|
39
|
+
setFormValues={setFormValues}
|
|
40
|
+
formValues={redirect}
|
|
41
|
+
addRedirect={vi.fn()}
|
|
42
|
+
currentFilter=""
|
|
43
|
+
isSiteItem={isSiteItem}
|
|
44
|
+
hoverCheck={false}
|
|
45
|
+
/>
|
|
46
|
+
</ThemeProvider>,
|
|
47
|
+
{ store },
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
describe("[UC-AX-manage-redirects] RedirectItem redirection type tag", () => {
|
|
51
|
+
it("AC8 · paints the redirect type on the row", () => {
|
|
52
|
+
renderItem({ type: "301" });
|
|
53
|
+
|
|
54
|
+
expect(screen.getByTestId("tag-square").textContent).toEqual("301");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("AC8 · leaves the cell empty, not absent, when the redirect has no type", () => {
|
|
58
|
+
renderItem();
|
|
59
|
+
|
|
60
|
+
expect(screen.queryByTestId("tag-square")).toBeNull();
|
|
61
|
+
// La celda tiene que seguir ocupando su hueco o la fila se descuadra
|
|
62
|
+
// respecto a la cabecera y a las filas que sí traen tipo.
|
|
63
|
+
expect(screen.getAllByRole("cell")[1].textContent).toEqual("");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("AC8 · keeps the column in place whether or not there is a type", () => {
|
|
67
|
+
// El recuento por sí solo no basta: borrar la columna baja los dos lados
|
|
68
|
+
// por igual. Hay que fijar también cuántas celdas tiene la fila.
|
|
69
|
+
const cellsInGlobalScope = 7;
|
|
70
|
+
|
|
71
|
+
const { unmount } = renderItem({ type: "301" });
|
|
72
|
+
expect(screen.getAllByRole("cell")).toHaveLength(cellsInGlobalScope);
|
|
73
|
+
unmount();
|
|
74
|
+
|
|
75
|
+
renderItem();
|
|
76
|
+
|
|
77
|
+
expect(screen.getAllByRole("cell")).toHaveLength(cellsInGlobalScope);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("AC8 · keeps the type column in site scope, without the site one", () => {
|
|
81
|
+
renderItem({ type: "301" }, true);
|
|
82
|
+
|
|
83
|
+
expect(screen.getByTestId("tag-square")).toBeTruthy();
|
|
84
|
+
expect(screen.queryByText("UST School")).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("[UC-AX-manage-redirects] TypeTag colours", () => {
|
|
89
|
+
const renderTag = (type: RedirectType) =>
|
|
90
|
+
render(
|
|
91
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
92
|
+
<S.TypeTag type="square" text={type} color={REDIRECT_TYPE_COLORS[type]} />
|
|
93
|
+
</ThemeProvider>,
|
|
94
|
+
{ store },
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
it("AC8 · paints 301 with the permanent colour", () => {
|
|
98
|
+
renderTag("301");
|
|
99
|
+
|
|
100
|
+
expect(screen.getByTestId("tag-square")).toHaveStyleRule("background-color", "#C9EFFF");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("AC8 · paints 302 with the temporary colour", () => {
|
|
104
|
+
renderTag("302");
|
|
105
|
+
|
|
106
|
+
expect(screen.getByTestId("tag-square")).toHaveStyleRule("background-color", "#DBB9FF");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("AC8 · keeps the text readable over the pastel background", () => {
|
|
110
|
+
// Tag paints a near-white text whenever `color` is set, unreadable on these
|
|
111
|
+
// backgrounds. TypeTag lo revierte con un `&&`, que duplica especificidad;
|
|
112
|
+
// hay que pedirle al matcher ese mismo selector, porque jsdom no aplica la
|
|
113
|
+
// cascada y sin `modifier` devuelve la regla plana de TagSquare.
|
|
114
|
+
renderTag("301");
|
|
115
|
+
|
|
116
|
+
expect(screen.getByTestId("tag-square")).toHaveStyleRule(
|
|
117
|
+
"color",
|
|
118
|
+
parseTheme(globalTheme).color.textMediumEmphasis,
|
|
119
|
+
{ modifier: "&&" },
|
|
120
|
+
);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("[UC-AX-manage-redirects] RedirectItem editing", () => {
|
|
125
|
+
it("AC7 · seeds the form with 301 when the row carries no type", () => {
|
|
126
|
+
const setFormValues = vi.fn();
|
|
127
|
+
renderItem({}, false, setFormValues);
|
|
128
|
+
|
|
129
|
+
fireEvent.click(screen.getByText("/about/team"));
|
|
130
|
+
|
|
131
|
+
expect(setFormValues).toHaveBeenCalledWith(expect.objectContaining({ type: "301" }));
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should open the row from the type cell too", () => {
|
|
135
|
+
// Todas las celdas de la fila abren la edición; si la del tipo se queda sin
|
|
136
|
+
// onClick, clicar el tag no hace nada y las demás sí.
|
|
137
|
+
const setFormValues = vi.fn();
|
|
138
|
+
renderItem({ type: "301" }, false, setFormValues);
|
|
139
|
+
|
|
140
|
+
fireEvent.click(screen.getByTestId("tag-square"));
|
|
141
|
+
|
|
142
|
+
expect(setFormValues).toHaveBeenCalledWith(expect.objectContaining({ type: "301" }));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("AC7 · keeps the type the row already has", () => {
|
|
146
|
+
const setFormValues = vi.fn();
|
|
147
|
+
renderItem({ type: "302" }, false, setFormValues);
|
|
148
|
+
|
|
149
|
+
fireEvent.click(screen.getByText("/about/team"));
|
|
150
|
+
|
|
151
|
+
expect(setFormValues).toHaveBeenCalledWith(expect.objectContaining({ type: "302" }));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("[UC-AX-manage-redirects] RedirectItem type normalisation", () => {
|
|
156
|
+
it("should paint the tag when the API sends the type as a number", () => {
|
|
157
|
+
// El store normaliza a string (containers/Redirects/actions), pero la fila
|
|
158
|
+
// no debe depender de ello para pintar.
|
|
159
|
+
renderItem({ type: 301 as never });
|
|
160
|
+
|
|
161
|
+
expect(screen.getByTestId("tag-square").textContent).toEqual("301");
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// `connect` captura el action creator al cargar el módulo, así que hay que
|
|
2
|
+
// sustituirlo antes de que se importe el panel (vi.mock se iza).
|
|
3
|
+
const updateRedirectSpy = vi.hoisted(() => vi.fn());
|
|
4
|
+
vi.mock("@ax/containers/Redirects", () => ({
|
|
5
|
+
redirectsActions: {
|
|
6
|
+
updateRedirect: (...args: unknown[]) => {
|
|
7
|
+
updateRedirectSpy(...args);
|
|
8
|
+
return () => Promise.resolve();
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
import { parseTheme } from "@ax/helpers";
|
|
14
|
+
import RedirectPanel from "@ax/modules/Redirects/RedirectPanel";
|
|
15
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
16
|
+
import type { IRedirect } from "@ax/types";
|
|
17
|
+
|
|
18
|
+
import configureStore from "redux-mock-store";
|
|
19
|
+
import thunk from "redux-thunk";
|
|
20
|
+
import { ThemeProvider } from "styled-components";
|
|
21
|
+
|
|
22
|
+
import { cleanup, fireEvent, render, screen } from "../../../../../config/tests/test-utils";
|
|
23
|
+
|
|
24
|
+
afterEach(cleanup);
|
|
25
|
+
|
|
26
|
+
const mockStore = configureStore([thunk]);
|
|
27
|
+
// El panel monta el UrlField, que arrastra el PageFinder conectado:
|
|
28
|
+
// necesita sites.currentSiteInfo/sites y app.lang/globalLangs.
|
|
29
|
+
const store = mockStore({
|
|
30
|
+
app: { isSaving: false, lang: { locale: "en_EN", id: 1 }, globalLangs: [] },
|
|
31
|
+
sites: { currentSiteInfo: null, sites: [{ id: 1, name: "UST School" }] },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const formValues: IRedirect = {
|
|
35
|
+
from: "/about/team",
|
|
36
|
+
to: { pageId: undefined, url: "/about/people" },
|
|
37
|
+
type: "301",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const renderPanel = (overrides: Partial<IRedirect> = {}, setFormValues = vi.fn()) => {
|
|
41
|
+
render(
|
|
42
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
43
|
+
<RedirectPanel
|
|
44
|
+
isOpen={true}
|
|
45
|
+
isOpenedSecond={false}
|
|
46
|
+
toggleModal={vi.fn()}
|
|
47
|
+
toggleSecondaryPanel={vi.fn()}
|
|
48
|
+
formValues={{ ...formValues, ...overrides }}
|
|
49
|
+
setFormValues={setFormValues}
|
|
50
|
+
addRedirect={vi.fn()}
|
|
51
|
+
currentFilter=""
|
|
52
|
+
/>
|
|
53
|
+
</ThemeProvider>,
|
|
54
|
+
{ store },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
return { setFormValues };
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
describe("[UC-AX-manage-redirects] RedirectPanel redirection type", () => {
|
|
61
|
+
it("AC7 · offers both types with their descriptions", () => {
|
|
62
|
+
renderPanel();
|
|
63
|
+
|
|
64
|
+
expect(screen.getByText("301 - Permanent")).toBeTruthy();
|
|
65
|
+
expect(screen.getByText("302 - Temporary")).toBeTruthy();
|
|
66
|
+
expect(screen.getByText("Keeps SEO. Use for definitive redirects.")).toBeTruthy();
|
|
67
|
+
expect(screen.getByText("Doesn’t keep SEO. Use for short-lived changes.")).toBeTruthy();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("AC7 · preselects the type held in the form", () => {
|
|
71
|
+
renderPanel();
|
|
72
|
+
|
|
73
|
+
const [permanent, temporary] = screen.getAllByTestId<HTMLInputElement>("radio-field-input");
|
|
74
|
+
expect(permanent.checked).toBe(true);
|
|
75
|
+
expect(temporary.checked).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("should not preselect anything when the type is not a string", () => {
|
|
79
|
+
// Documenta por qué el store normaliza: RadioGroup compara con ===, así que
|
|
80
|
+
// un 301 numérico no casa con la opción "301" y el panel sale en blanco.
|
|
81
|
+
renderPanel({ type: 301 as never });
|
|
82
|
+
|
|
83
|
+
const [permanent, temporary] = screen.getAllByTestId<HTMLInputElement>("radio-field-input");
|
|
84
|
+
expect(permanent.checked).toBe(false);
|
|
85
|
+
expect(temporary.checked).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("AC7 · preselects 302 when the form holds it", () => {
|
|
89
|
+
renderPanel({ type: "302" });
|
|
90
|
+
|
|
91
|
+
const [permanent, temporary] = screen.getAllByTestId<HTMLInputElement>("radio-field-input");
|
|
92
|
+
expect(permanent.checked).toBe(false);
|
|
93
|
+
expect(temporary.checked).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("AC7 · reports the chosen type without touching the rest of the form", () => {
|
|
97
|
+
const { setFormValues } = renderPanel();
|
|
98
|
+
|
|
99
|
+
const [, temporary] = screen.getAllByTestId("radio-field-input");
|
|
100
|
+
fireEvent.click(temporary);
|
|
101
|
+
|
|
102
|
+
expect(setFormValues).toHaveBeenCalledWith({ ...formValues, type: "302" });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("[UC-AX-manage-redirects] RedirectPanel update payload", () => {
|
|
107
|
+
it("AC7 · sends the chosen type on update (PUT)", async () => {
|
|
108
|
+
render(
|
|
109
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
110
|
+
<RedirectPanel
|
|
111
|
+
isOpen={true}
|
|
112
|
+
isOpenedSecond={false}
|
|
113
|
+
toggleModal={vi.fn()}
|
|
114
|
+
toggleSecondaryPanel={vi.fn()}
|
|
115
|
+
redirect={{ ...formValues, id: 7 }}
|
|
116
|
+
formValues={{ ...formValues, id: 7, type: "302" }}
|
|
117
|
+
setFormValues={vi.fn()}
|
|
118
|
+
addRedirect={vi.fn()}
|
|
119
|
+
currentFilter="&sites=1"
|
|
120
|
+
/>
|
|
121
|
+
</ThemeProvider>,
|
|
122
|
+
{ store },
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
// "Update Redirect" es también el título del panel; hay que ir al botón.
|
|
126
|
+
fireEvent.click(screen.getByRole("button", { name: "Update Redirect" }));
|
|
127
|
+
|
|
128
|
+
expect(updateRedirectSpy).toHaveBeenCalledWith(
|
|
129
|
+
{ from: "/about/team", to: "/about/people", type: "302" },
|
|
130
|
+
7,
|
|
131
|
+
"&sites=1",
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// `connect` captura los action creators al cargar el módulo, así que hay que
|
|
2
|
+
// sustituirlos antes de que se importe la pantalla (vi.mock se iza).
|
|
3
|
+
const addRedirectSpy = vi.hoisted(() => vi.fn());
|
|
4
|
+
vi.mock("@ax/containers/Redirects", () => ({
|
|
5
|
+
redirectsActions: {
|
|
6
|
+
getRedirects: () => () => Promise.resolve(),
|
|
7
|
+
deleteRedirect: () => () => Promise.resolve(true),
|
|
8
|
+
importRedirects: () => () => Promise.resolve(true),
|
|
9
|
+
updateRedirect: () => () => Promise.resolve(),
|
|
10
|
+
addRedirect: (...args: unknown[]) => {
|
|
11
|
+
addRedirectSpy(...args);
|
|
12
|
+
return () => Promise.resolve();
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
import { MemoryRouter } from "react-router-dom";
|
|
18
|
+
|
|
19
|
+
import { parseTheme } from "@ax/helpers";
|
|
20
|
+
import Redirects from "@ax/modules/Redirects";
|
|
21
|
+
import globalTheme from "@ax/themes/theme.json";
|
|
22
|
+
|
|
23
|
+
import configureStore from "redux-mock-store";
|
|
24
|
+
import thunk from "redux-thunk";
|
|
25
|
+
import { ThemeProvider } from "styled-components";
|
|
26
|
+
|
|
27
|
+
import { cleanup, fireEvent, render, screen } from "../../../../../config/tests/test-utils";
|
|
28
|
+
|
|
29
|
+
afterEach(cleanup);
|
|
30
|
+
|
|
31
|
+
// jsdom no implementa Element.scrollTo y el módulo lo llama sobre el tableRef.
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
Element.prototype.scrollTo = vi.fn();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const store = configureStore([thunk])({
|
|
37
|
+
redirects: { redirects: [], totalItems: 0, totalImports: 0, imports: null },
|
|
38
|
+
sites: { currentSiteInfo: null, sites: [{ id: 1, name: "UST School" }] },
|
|
39
|
+
app: { isLoading: false, isSaving: false, lang: { locale: "en_EN", id: 1 }, globalLangs: [] },
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const navItems = [{ title: "Redirects", path: "/settings/redirects" }];
|
|
43
|
+
|
|
44
|
+
describe("[UC-AX-manage-redirects] Redirects — crear con tipo", () => {
|
|
45
|
+
it("AC7 · creates as 301 when the author does not touch the type (POST)", () => {
|
|
46
|
+
render(
|
|
47
|
+
<MemoryRouter>
|
|
48
|
+
<ThemeProvider theme={parseTheme(globalTheme)}>
|
|
49
|
+
<Redirects navItems={navItems as never} currentNavItem={navItems[0] as never} />
|
|
50
|
+
</ThemeProvider>
|
|
51
|
+
</MemoryRouter>,
|
|
52
|
+
{ store },
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
fireEvent.click(screen.getByText("New Redirect"));
|
|
56
|
+
// Hay varios inputs en pantalla (el buscador del MainWrapper incluido);
|
|
57
|
+
// el autoComplete del panel es el que identifica el campo "Old URL".
|
|
58
|
+
const oldUrl = document.querySelector('input[autocomplete="redirect-old"]') as HTMLInputElement;
|
|
59
|
+
fireEvent.change(oldUrl, { target: { value: "/old" } });
|
|
60
|
+
fireEvent.click(screen.getByRole("button", { name: "Add Redirect" }));
|
|
61
|
+
|
|
62
|
+
expect(addRedirectSpy).toHaveBeenCalledWith(
|
|
63
|
+
expect.objectContaining({ type: "301" }),
|
|
64
|
+
expect.any(Function),
|
|
65
|
+
undefined,
|
|
66
|
+
expect.any(String),
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|