@griddo/ax 12.10.5 → 12.11.1-rc.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.
Files changed (33) hide show
  1. package/README.md +0 -2
  2. package/config/webpack.config.js +2 -1
  3. package/config/webpackSchemas.config.js +7 -4
  4. package/package.json +31 -56
  5. package/src/__tests__/components/Fields/ConditionalField/ConditionalField.test.tsx +17 -0
  6. package/src/__tests__/components/Fields/RadioField/RadioField.test.tsx +66 -0
  7. package/src/__tests__/components/Fields/RadioGroup/RadioGroup.test.tsx +76 -0
  8. package/src/__tests__/modules/Redirects/BulkHeader/TableHeader.test.tsx +123 -0
  9. package/src/__tests__/modules/Redirects/RedirectItem/RedirectItem.test.tsx +163 -0
  10. package/src/__tests__/modules/Redirects/RedirectPanel/RedirectPanel.test.tsx +134 -0
  11. package/src/__tests__/modules/Redirects/Redirects/Redirects.create.test.tsx +69 -0
  12. package/src/__tests__/modules/Redirects/Redirects/Redirects.edit.test.tsx +91 -0
  13. package/src/__tests__/modules/Redirects/Redirects/Redirects.filter.test.tsx +131 -0
  14. package/src/components/Fields/ConditionalField/style.tsx +1 -1
  15. package/src/components/Fields/RadioField/index.tsx +20 -3
  16. package/src/components/Fields/RadioField/style.tsx +18 -1
  17. package/src/components/Fields/RadioGroup/index.tsx +1 -0
  18. package/src/containers/Redirects/actions.tsx +15 -3
  19. package/src/global.d.ts +0 -1
  20. package/src/modules/ActivityLog/DownloadModal/index.tsx +2 -1
  21. package/src/modules/ActivityLog/index.tsx +2 -2
  22. package/src/modules/Redirects/BulkHeader/TableHeader/index.tsx +5 -0
  23. package/src/modules/Redirects/BulkHeader/TableHeader/style.tsx +5 -1
  24. package/src/modules/Redirects/RedirectItem/index.tsx +11 -1
  25. package/src/modules/Redirects/RedirectItem/style.tsx +29 -1
  26. package/src/modules/Redirects/RedirectPanel/index.tsx +40 -3
  27. package/src/modules/Redirects/TypeFilter/index.tsx +44 -0
  28. package/src/modules/Redirects/TypeFilter/style.tsx +27 -0
  29. package/src/modules/Redirects/hooks.tsx +12 -4
  30. package/src/modules/Redirects/index.tsx +22 -14
  31. package/src/modules/Redirects/utils.tsx +10 -1
  32. package/src/types/index.tsx +3 -0
  33. package/tsconfig.json +1 -9
package/README.md CHANGED
@@ -25,8 +25,6 @@
25
25
  - `yarn test:watch` - Ejecuta los tests en modo watch
26
26
  - `yarn test:ci` - Ejecuta los tests en modo CI (sin paralelización)
27
27
  - `yarn test:coverage` - Ejecuta los tests y genera reporte de cobertura
28
- - `yarn test:dev` - Ejecuta los tests en modo watch (alias de `test:watch`)
29
- - `yarn test:dev:coverage` - Ejecuta los tests con cobertura en modo watch
30
28
 
31
29
  ### Linting y Formato
32
30
 
@@ -381,7 +381,8 @@ module.exports = function (webpackEnv) {
381
381
  {
382
382
  test: /\.(js|mjs|jsx|ts|tsx)$/,
383
383
  include: [paths.appSrc, paths.componentsPath],
384
- exclude: [/node_modules\/(?!@griddo).*/],
384
+ // pnpm installs ax under node_modules/.pnpm/<id>/node_modules/@griddo/ax.
385
+ exclude: [/node_modules\/(?!(\.pnpm\/[^/]+\/node_modules\/)?@griddo).*/],
385
386
  loader: require.resolve("babel-loader"),
386
387
  options: {
387
388
  customize: require.resolve("babel-preset-react-app/webpack-overrides"),
@@ -24,10 +24,13 @@ const createConfig = ({ input, output }) => ({
24
24
  resolve: { extensions: [".ts", ".js"] },
25
25
  exclude: /(node_modules|bower_components)/,
26
26
  use: {
27
- loader: "babel-loader",
27
+ loader: require.resolve("babel-loader"),
28
28
  options: {
29
- presets: [["@babel/preset-env", { targets: { node: "current" } }], "@babel/preset-typescript"],
30
- plugins: ["@babel/plugin-transform-optional-chaining"],
29
+ presets: [
30
+ [require.resolve("@babel/preset-env"), { targets: { node: "current" } }],
31
+ require.resolve("@babel/preset-typescript"),
32
+ ],
33
+ plugins: [require.resolve("@babel/plugin-transform-optional-chaining")],
31
34
  },
32
35
  },
33
36
  },
@@ -36,7 +39,7 @@ const createConfig = ({ input, output }) => ({
36
39
  // API crashing (it hasn't webpack)
37
40
  {
38
41
  test: /\.(png|jpe?g|gif)$/i,
39
- use: [{ loader: "ignore-loader" }],
42
+ use: [{ loader: require.resolve("ignore-loader") }],
40
43
  },
41
44
  ],
42
45
  },
package/package.json CHANGED
@@ -1,15 +1,7 @@
1
1
  {
2
2
  "name": "@griddo/ax",
3
3
  "description": "Griddo Author Experience",
4
- "version": "12.10.5",
5
- "authors": [
6
- "Álvaro Sánchez' <alvaro.sanches@secuoyas.com>",
7
- "Diego M. Béjar <diego.bejar@secuoyas.com>",
8
- "Francis Vega <francis.vega@secuoyas.com>",
9
- "Gonzalo Hernández <gonzalo.hernandez@secuoyas.com>",
10
- "Sergio Ródenas <sergio.rodenas@secuoyas.com>",
11
- "Txema León <txema.leon@secuoyas.com>"
12
- ],
4
+ "version": "12.11.1-rc.0",
13
5
  "homepage": "https://griddo.io",
14
6
  "repository": {
15
7
  "type": "git",
@@ -20,62 +12,57 @@
20
12
  "griddo-sync-schemas": "./scripts/griddo-sync-schemas.js"
21
13
  },
22
14
  "scripts": {
23
- "// START": "",
24
15
  "start": "npm run start:dev",
25
16
  "start:local": "node --env-file=./env/.env.local ./scripts/start.js",
26
17
  "start:dev": "node --env-file=./env/.env.development ./scripts/start.js",
27
18
  "start:staging": "node --env-file=./env/.env.staging ./scripts/start.js",
28
- "// BUILD": "",
29
19
  "build": "node ./scripts/build.js",
30
20
  "build:dev": "node --env-file=./env/.env.development ./scripts/build.js",
31
- "// TEST": "",
32
21
  "test": "vitest run",
33
22
  "test:watch": "vitest",
34
23
  "test:ci": "vitest run --no-file-parallelism",
35
24
  "test:coverage": "vitest run --coverage",
36
- "test:dev": "vitest",
37
- "test:dev:coverage": "vitest --coverage",
38
- "// TYPES": "",
39
25
  "typecheck": "tsc --noEmit",
40
- "// LINT": "",
41
26
  "lint": "biome check",
42
27
  "format:write": "biome format --write"
43
28
  },
44
29
  "dependencies": {
30
+ "@babel/core": "7.29.7",
31
+ "@babel/plugin-transform-optional-chaining": "7.29.7",
32
+ "@babel/preset-env": "7.29.7",
33
+ "@babel/preset-typescript": "7.29.7",
45
34
  "@dnd-kit/core": "6.3.1",
46
35
  "@dnd-kit/sortable": "10.0.0",
47
- "@griddo/api-types": "1.1.3",
36
+ "@dnd-kit/utilities": "3.2.2",
37
+ "@griddo/api-types": "1.3.2",
48
38
  "@pmmmwh/react-refresh-webpack-plugin": "0.5.17",
49
- "@styled-system/prop-types": "5.1.5",
50
- "@styled-system/theme-get": "5.1.2",
51
39
  "@svgr/webpack": "8.1.0",
52
40
  "@testing-library/jest-dom": "5.17.0",
53
41
  "@testing-library/react": "13.4.0",
54
- "@testing-library/user-event": "14.6.6",
42
+ "@testing-library/user-event": "14.6.7",
55
43
  "@types/compress.js": "1.1.3",
56
44
  "@types/draft-js": "0.11.20",
57
45
  "@types/lodash.isequal": "4.5.8",
58
46
  "@types/markdown-draft-js": "2.2.7",
59
- "@types/node": "20.19.43",
47
+ "@types/node": "24.12.4",
60
48
  "@types/react": "18.2.79",
61
- "@types/react-beautiful-dnd": "13.1.8",
62
49
  "@types/react-datepicker": "4.19.6",
63
50
  "@types/react-dom": "18.2.25",
64
51
  "@types/react-draft-wysiwyg": "1.13.9",
65
52
  "@types/react-redux": "7.1.34",
53
+ "@types/react-router": "5.1.20",
66
54
  "@types/react-router-dom": "5.3.3",
67
55
  "@types/react-slick": "0.23.13",
68
- "@types/react-test-renderer": "17.0.9",
69
56
  "@types/styled-components": "5.1.36",
57
+ "@types/testing-library__jest-dom": "5.14.9",
70
58
  "@types/uuid": "8.3.4",
71
- "@types/webpack-env": "1.18.8",
72
59
  "axios": "0.19.0",
73
60
  "babel-loader": "9.2.1",
74
61
  "babel-plugin-named-asset-import": "0.3.8",
75
- "babel-plugin-root-import": "6.6.0",
76
62
  "babel-plugin-styled-components": "2.3.0",
77
63
  "babel-preset-react-app": "10.1.0",
78
64
  "case-sensitive-paths-webpack-plugin": "2.4.0",
65
+ "chalk": "4.1.2",
79
66
  "compress.js": "1.2.2",
80
67
  "connected-react-router": "6.9.3",
81
68
  "css-loader": "6.11.0",
@@ -86,30 +73,28 @@
86
73
  "draft-js": "0.11.7",
87
74
  "draft-js-import-html": "1.4.1",
88
75
  "draftjs-to-html": "0.9.1",
89
- "enhanced-resolve": "5.18.1",
90
- "env-cmd": "10.1.0",
91
76
  "find-up": "5.0.0",
77
+ "froala-editor": "4.0.4",
92
78
  "fs-extra": "7.0.1",
79
+ "history": "4.10.1",
93
80
  "html-to-draftjs": "1.5.0",
94
- "html-to-image": "1.11.7",
81
+ "html-to-image": "1.11.13",
95
82
  "html-webpack-plugin": "5.6.8",
96
83
  "ignore-loader": "0.1.2",
97
- "is-wsl": "3.1.0",
84
+ "immutable": "3.7.6",
98
85
  "jsdom": "20.0.3",
99
- "jsdom-global": "3.0.2",
100
86
  "lodash.isequal": "4.5.0",
101
87
  "markdown-draft-js": "2.4.0",
102
88
  "mini-css-extract-plugin": "2.10.2",
103
89
  "node-polyfill-webpack-plugin": "3.0.0",
104
90
  "pkg-dir": "5.0.0",
105
91
  "polished": "3.7.2",
106
- "postcss": "8.5.26",
92
+ "postcss": "8.5.28",
107
93
  "postcss-flexbugs-fixes": "5.0.2",
108
94
  "postcss-loader": "7.3.4",
109
95
  "postcss-normalize": "7.0.1",
110
96
  "postcss-preset-env": "6.7.2",
111
97
  "react": "18.2.0",
112
- "react-app-polyfill": "1.0.6",
113
98
  "react-datepicker": "4.25.0",
114
99
  "react-dev-utils": "12.0.1",
115
100
  "react-dom": "18.2.0",
@@ -120,6 +105,7 @@
120
105
  "react-qr-code": "2.2.0",
121
106
  "react-redux": "7.2.9",
122
107
  "react-refresh": "0.14.2",
108
+ "react-router": "5.3.4",
123
109
  "react-router-dom": "5.3.4",
124
110
  "react-select": "5.10.2",
125
111
  "react-slick": "0.30.3",
@@ -128,50 +114,37 @@
128
114
  "redux": "4.2.1",
129
115
  "redux-devtools-extension": "2.13.9",
130
116
  "redux-persist": "6.0.0",
131
- "reflect-metadata": "0.1.13",
117
+ "redux-thunk": "2.4.2",
132
118
  "resolve": "1.22.12",
133
119
  "resolve-url-loader": "4.0.0",
134
- "sass-alias": "1.0.5",
135
120
  "sass-loader": "16.0.8",
136
121
  "semver": "7.8.5",
137
122
  "slick-carousel": "1.8.1",
138
- "source-map-loader": "1.1.3",
139
- "string-replace-loader": "3.1.0",
140
123
  "style-loader": "3.3.4",
141
124
  "styled-components": "5.3.11",
142
125
  "styled-reset": "4.0.1",
143
- "styled-system": "5.1.5",
144
126
  "terser-webpack-plugin": "5.6.1",
145
127
  "typescript": "6.0.3",
146
128
  "uuid": "8.3.2",
147
- "webpack": "5.109.2",
129
+ "webpack": "5.111.1",
148
130
  "webpack-dev-server": "4.15.2",
149
131
  "webpack-manifest-plugin": "5.0.1"
150
132
  },
151
133
  "devDependencies": {
152
- "@babel/core": "7.29.7",
153
- "@babel/helper-environment-visitor": "7.24.7",
154
- "@babel/helper-function-name": "7.24.7",
155
- "@babel/helper-hoist-variables": "7.24.7",
156
- "@babel/helper-split-export-declaration": "7.24.7",
157
- "@babel/plugin-transform-optional-chaining": "7.29.7",
158
- "@babel/preset-env": "7.29.7",
159
- "@babel/preset-react": "7.29.7",
160
- "@babel/preset-typescript": "7.29.7",
161
134
  "@biomejs/biome": "2.3.8",
135
+ "@griddo/core": "12.11.1-rc.0",
162
136
  "@types/redux-mock-store": "1.5.0",
163
- "@vitest/coverage-v8": "4.1.10",
137
+ "@vitest/coverage-v8": "4.1.11",
164
138
  "axios-mock-adapter": "1.22.0",
165
- "prettier": "3.4.2",
166
- "react-test-renderer": "18.2.0",
167
139
  "redux-mock-store": "1.5.5",
168
- "redux-thunk": "2.4.2",
169
- "ts-node": "10.9.2",
170
- "vite": "7.3.6",
171
- "vitest": "4.1.10",
140
+ "vite": "8.3.1",
141
+ "vitest": "4.1.11",
172
142
  "vitest-mock-extended": "5.1.1",
173
143
  "vitest-styled-components": "1.0.0"
174
144
  },
145
+ "peerDependencies": {
146
+ "@griddo/core": ">=12 <13"
147
+ },
175
148
  "babel": {
176
149
  "presets": [
177
150
  "react-app"
@@ -189,9 +162,11 @@
189
162
  "last 1 safari version"
190
163
  ]
191
164
  },
165
+ "engines": {
166
+ "node": ">=24.12.0"
167
+ },
192
168
  "files": [
193
169
  "config",
194
- "patches",
195
170
  "public",
196
171
  "scripts",
197
172
  "src",
@@ -201,5 +176,5 @@
201
176
  "publishConfig": {
202
177
  "access": "public"
203
178
  },
204
- "gitHead": "9ec0a833c7547915d64745182be330df4bfc1e9c"
179
+ "gitHead": "7ac7ef01e60f52a92be0b5414c50cd60e5f0714e"
205
180
  }
@@ -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
+ });