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

mendersoftware / gui / 951400782

pending completion
951400782

Pull #3900

gitlab-ci

web-flow
chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 5.16.5 to 5.17.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v5.16.5...v5.17.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #3900: chore: bump @testing-library/jest-dom from 5.16.5 to 5.17.0

4446 of 6414 branches covered (69.32%)

8342 of 10084 relevant lines covered (82.73%)

186.0 hits per line

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

94.21
/src/js/utils/locationutils.js
1
// Copyright 2022 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14
import { routes } from '../components/devices/base-devices';
15
import { DEPLOYMENT_ROUTES, DEPLOYMENT_STATES, DEPLOYMENT_TYPES } from '../constants/deploymentConstants';
16
import { ATTRIBUTE_SCOPES, DEVICE_FILTERING_OPTIONS, DEVICE_LIST_DEFAULTS, UNGROUPED_GROUP, emptyFilter } from '../constants/deviceConstants';
17
import { AUDIT_LOGS_TYPES } from '../constants/organizationConstants';
18
import { deepCompare, getISOStringBoundaries } from '../helpers';
19

20
const SEPARATOR = ':';
25✔
21

22
const defaultSelector = result => result[0];
25✔
23

24
const commonFields = {
25✔
25
  ...Object.keys(DEVICE_LIST_DEFAULTS).reduce((accu, key) => ({ ...accu, [key]: { parse: Number, select: defaultSelector } }), {}),
50✔
26
  id: { parse: String, select: i => i },
3✔
27
  issues: { parse: undefined, select: defaultSelector },
28
  open: { parse: Boolean, select: defaultSelector }
29
};
30

31
const scopes = {
25✔
32
  identity: { delimiter: 'identity', filters: [] },
33
  inventory: { delimiter: 'inventory', filters: [] },
34
  monitor: { delimiter: 'monitor', filters: [] },
35
  system: { delimiter: 'system', filters: [] },
36
  tags: { delimiter: 'tags', filters: [] }
37
};
38

39
export const commonProcessor = searchParams => {
25✔
40
  let params = new URLSearchParams(searchParams);
49✔
41
  const pageState = Object.entries(commonFields).reduce((accu, [key, { parse, select }]) => {
49✔
42
    const values = params.getAll(key);
245✔
43
    if (!values.length) {
245✔
44
      return accu;
233✔
45
    }
46
    if (!parse) {
12✔
47
      accu[key] = values;
1✔
48
    } else {
49
      try {
11✔
50
        accu[key] = select(values.map(parse));
11✔
51
      } catch (error) {
52
        console.log('encountered faulty url param, continue...', error);
×
53
      }
54
    }
55
    return accu;
12✔
56
  }, {});
57
  Object.keys(commonFields).map(key => params.delete(key));
245✔
58
  const sort = params.has('sort')
49✔
59
    ? params.getAll('sort').reduce((sortAccu, scopedQuery) => {
60
        const items = scopedQuery.split(SEPARATOR).reverse();
13✔
61
        return ['direction', 'key', 'scope'].reduce((accu, key, index) => {
13✔
62
          if (items[index]) {
39✔
63
            accu[key] = items[index];
9✔
64
          }
65
          return accu;
39✔
66
        }, sortAccu);
67
      }, {})
68
    : undefined;
69
  params.delete('sort');
49✔
70
  return { pageState, params, sort };
49✔
71
};
72

73
const legacyDeviceQueryParse = (searchParams, filteringAttributes) => {
25✔
74
  let params = new URLSearchParams(searchParams);
1✔
75
  const result = Object.keys(scopes).reduce((accu, scope) => ({ ...accu, [scope]: [] }), {});
5✔
76
  if (params.get('group')) {
1!
77
    result.inventory.push({ ...emptyFilter, key: 'group', scope: 'inventory', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: params.get('group') });
1✔
78
    params.delete('group');
1✔
79
  }
80
  const filters = [...params.keys()].reduce(
1✔
81
    (accu, key) =>
82
      params.getAll(key).reduce((innerAccu, value) => {
3✔
83
        const scope =
84
          Object.entries(filteringAttributes).reduce((foundScope, [currentScope, attributes]) => {
3✔
85
            if (foundScope) {
6✔
86
              return foundScope;
1✔
87
            }
88
            return attributes.includes(key) ? currentScope.substring(0, currentScope.indexOf('Attributes')) : foundScope;
5✔
89
          }, undefined) ?? ATTRIBUTE_SCOPES.inventory;
90
        innerAccu[scope].push({ ...emptyFilter, scope, key, operator: DEVICE_FILTERING_OPTIONS.$eq.key, value });
3✔
91
        return innerAccu;
3✔
92
      }, accu),
93
    result
94
  );
95
  [...params.keys()].map(key => params.delete(key));
3✔
96
  return { filters, params };
1✔
97
};
98

99
const scopedFilterParse = searchParams => {
25✔
100
  let params = new URLSearchParams(searchParams);
4✔
101
  const filters = Object.keys(scopes).reduce(
4✔
102
    (accu, scope) => {
103
      accu[scope] = [];
20✔
104
      if (!params.has(scope)) {
20✔
105
        return accu;
17✔
106
      }
107
      accu[scope] = params.getAll(scope).map(scopedQuery => {
3✔
108
        const items = scopedQuery.split(SEPARATOR);
4✔
109
        // URLSearchParams will automatically decode any URI encoding present in the query string, thus we have to also handle queries with a SEPARATOR separately
110
        return { ...emptyFilter, scope, key: items[0], operator: `$${items[1]}`, value: items.slice(2).join(SEPARATOR) };
4✔
111
      });
112
      return accu;
3✔
113
    },
114
    { ...scopes }
115
  );
116
  Object.keys(scopes).map(scope => params.delete(scope));
20✔
117
  return { filters, params };
4✔
118
};
119

120
// filters, selectedGroup
121
export const parseDeviceQuery = (searchParams, extraProps = {}) => {
25✔
122
  let queryParams = new URLSearchParams(searchParams);
5✔
123
  const { filteringAttributes = {}, pageState = {} } = extraProps;
5✔
124
  const pageStateExtension = pageState.id?.length === 1 ? { open: true } : {};
5✔
125

126
  let scopedFilters;
127
  const refersOldStyleAttributes = Object.values(filteringAttributes).some(scopeValues => scopeValues.some(scopedValue => queryParams.get(scopedValue)));
5✔
128
  if ((refersOldStyleAttributes && !Object.keys(scopes).some(scope => queryParams.get(scope))) || queryParams.get('group')) {
5✔
129
    const { filters, params } = legacyDeviceQueryParse(queryParams, filteringAttributes);
1✔
130
    scopedFilters = filters;
1✔
131
    queryParams = params;
1✔
132
  } else {
133
    const { filters, params } = scopedFilterParse(queryParams);
4✔
134
    scopedFilters = filters;
4✔
135
    queryParams = params;
4✔
136
  }
137

138
  let groupName = '';
5✔
139
  const groupFilterIndex = scopedFilters.inventory.findIndex(filter => filter.key === 'group' && filter.operator === DEVICE_FILTERING_OPTIONS.$eq.key);
5✔
140
  if (groupFilterIndex > -1) {
5✔
141
    groupName = scopedFilters.inventory[groupFilterIndex].value;
2✔
142
    scopedFilters.inventory.splice(groupFilterIndex, 1);
2✔
143
  }
144

145
  const detailsTab = queryParams.has('tab') ? queryParams.get('tab') : '';
5!
146
  return { detailsTab, filters: Object.values(scopedFilters).flat(), groupName, ...pageStateExtension };
5✔
147
};
148

149
const formatSorting = (sort, { sort: sortDefault }) => {
25✔
150
  if (!sort || deepCompare(sort, sortDefault)) {
45✔
151
    return '';
29✔
152
  }
153
  const sortQuery = ['scope', 'key', 'direction']
16✔
154
    .reduce((accu, key) => {
155
      if (!sort[key]) {
48✔
156
        return accu;
36✔
157
      }
158
      accu.push(sort[key]);
12✔
159
      return accu;
12✔
160
    }, [])
161
    .join(SEPARATOR);
162
  return `sort=${sortQuery}`;
16✔
163
};
164

165
export const formatPageState = ({ selectedId, selectedIssues, page, perPage, sort }, { defaults }) =>
25✔
166
  Object.entries({ page, perPage, id: selectedId, issues: selectedIssues, open: selectedId ? true : undefined })
45✔
167
    .reduce(
168
      (accu, [key, value]) => {
169
        if (Array.isArray(value)) {
225✔
170
          accu.push(...value.map(item => `${key}=${encodeURIComponent(item)}`));
2✔
171
        } else if ((DEVICE_LIST_DEFAULTS[key] != value || !DEVICE_LIST_DEFAULTS.hasOwnProperty(key)) && value) {
224✔
172
          accu.push(`${key}=${encodeURIComponent(value)}`);
10✔
173
        }
174
        return accu;
225✔
175
      },
176
      [formatSorting(sort, defaults)]
177
    )
178
    .filter(i => i)
57✔
179
    .join('&');
180

181
const stripFilterOperator = operator => operator.replaceAll('$', '');
25✔
182

183
const formatFilters = filters => {
25✔
184
  const result = filters
3✔
185
    // group all filters by their scope to get a more organized result
186
    .reduce(
187
      (accu, filter) => {
188
        const { scope = ATTRIBUTE_SCOPES.inventory, operator = '$eq' } = filter;
6✔
189
        accu[scope].add(`${scopes[scope].delimiter}=${filter.key}${SEPARATOR}${stripFilterOperator(operator)}${SEPARATOR}${encodeURIComponent(filter.value)}`);
6✔
190
        return accu;
6✔
191
      },
192
      Object.keys(scopes).reduce((accu, item) => ({ ...accu, [item]: new Set() }), {})
15✔
193
    );
194
  // boil it all down to a single line containing all filters
195
  return Object.values(result)
3✔
196
    .map(filterSet => [...filterSet])
15✔
197
    .flat();
198
};
199

200
export const formatDeviceSearch = ({ pageState, filters, selectedGroup }) => {
25✔
201
  let activeFilters = [...filters];
3✔
202
  if (selectedGroup) {
3!
203
    const isUngroupedGroup = selectedGroup === UNGROUPED_GROUP.id;
3✔
204
    activeFilters = isUngroupedGroup
3✔
205
      ? activeFilters.filter(
206
          filter => !(filter.key === 'group' && filter.scope === ATTRIBUTE_SCOPES.system && filter.operator === DEVICE_FILTERING_OPTIONS.$nin.key)
1!
207
        )
208
      : activeFilters;
209
    const groupName = isUngroupedGroup ? UNGROUPED_GROUP.name : selectedGroup;
3✔
210
    activeFilters.push({ scope: ATTRIBUTE_SCOPES.inventory, key: 'group', operator: DEVICE_FILTERING_OPTIONS.$eq.key, value: groupName });
3✔
211
  }
212
  const formattedFilters = formatFilters(activeFilters).filter(i => i);
6✔
213
  if (pageState.detailsTab && pageState.selectedId) {
3!
214
    formattedFilters.push(`tab=${pageState.detailsTab}`);
×
215
  }
216
  return formattedFilters.join('&');
3✔
217
};
218

219
export const generateDevicePath = ({ pageState }) => {
25✔
220
  const { state: selectedState } = pageState;
2✔
221
  const path = ['/devices'];
2✔
222
  if (![routes.allDevices.key, ''].includes(selectedState)) {
2!
223
    path.push(selectedState);
2✔
224
  }
225
  return path.join('/');
2✔
226
};
227

228
const formatDates = ({ endDate, params, startDate, today, tonight }) => {
25✔
229
  if (endDate && endDate !== tonight) {
25✔
230
    params.set('endDate', new Date(endDate).toISOString().split('T')[0]);
2✔
231
  }
232
  if (startDate && startDate !== today) {
25✔
233
    params.set('startDate', new Date(startDate).toISOString().split('T')[0]);
13✔
234
  }
235
  return params;
25✔
236
};
237

238
const paramReducer = (accu, [key, value]) => {
25✔
239
  if (value) {
50✔
240
    accu.set(key, value);
13✔
241
  }
242
  return accu;
50✔
243
};
244

245
export const formatAuditlogs = ({ pageState }, { today, tonight }) => {
25✔
246
  const { detail, endDate, startDate, type = null, user = null } = pageState;
19✔
247
  let params = new URLSearchParams();
19✔
248
  params = Object.entries({ objectId: detail, userId: user ? user.id ?? user : user }).reduce(paramReducer, params);
19✔
249
  if (type) {
19✔
250
    params.set('objectType', type.value ?? type);
9✔
251
  }
252
  params = formatDates({ endDate, params, startDate, today, tonight });
19✔
253
  return params.toString();
19✔
254
};
255

256
const parseDateParams = (params, today, tonight) => {
25✔
257
  let endDate = tonight;
46✔
258
  if (params.get('endDate')) {
46✔
259
    endDate = getISOStringBoundaries(new Date(params.get('endDate'))).end;
2✔
260
  }
261
  let startDate = today;
46✔
262
  if (params.get('startDate')) {
46✔
263
    startDate = getISOStringBoundaries(new Date(params.get('startDate'))).start;
7✔
264
  }
265
  return { endDate, startDate };
46✔
266
};
267

268
export const parseAuditlogsQuery = (params, { today, tonight }) => {
25✔
269
  const type = AUDIT_LOGS_TYPES.find(typeObject => typeObject.value === params.get('objectType')) || null;
55✔
270
  const { endDate, startDate } = parseDateParams(params, today, tonight);
14✔
271
  return {
14✔
272
    detail: params.get('objectId'),
273
    endDate,
274
    startDate,
275
    type,
276
    user: params.get('userId')
277
  };
278
};
279

280
const formatActiveDeployments = (pageState, { defaults }) =>
25✔
281
  [DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.pending]
16✔
282
    .reduce((accu, state) => {
283
      const { page, perPage } = pageState[state] ?? {};
32!
284
      const stateDefaults = defaults[state] ?? {};
32!
285
      const items = Object.entries({ page, perPage })
32✔
286
        .reverse()
287
        .reduce((keyAccu, [key, value]) => {
288
          if ((value && value !== stateDefaults[key]) || keyAccu.length) {
64!
289
            keyAccu.unshift(value || stateDefaults[key]);
64✔
290
          }
291
          return keyAccu;
64✔
292
        }, []);
293
      if (items.length) {
32!
294
        accu.push(`${state}=${items.join(SEPARATOR)}`);
32✔
295
      }
296
      return accu;
32✔
297
    }, [])
298
    .filter(i => i)
32✔
299
    .join('&');
300

301
export const formatDeployments = ({ deploymentObject, pageState }, { defaults, today, tonight }) => {
25✔
302
  const { state: selectedState, showCreationDialog } = pageState.general;
24✔
303
  let params = new URLSearchParams();
24✔
304
  if (showCreationDialog) {
24✔
305
    params.set('open', true);
5✔
306
    if (deploymentObject.release) {
5!
307
      params.set('release', deploymentObject.release.Name);
×
308
    }
309
    if (deploymentObject.devices?.length) {
5!
310
      deploymentObject.devices.map(({ id }) => params.append('deviceId', id));
×
311
    }
312
  }
313
  let pageStateQuery;
314
  if (selectedState === DEPLOYMENT_ROUTES.finished.key) {
24✔
315
    const { endDate, search, startDate, type } = pageState[selectedState];
6✔
316
    params = formatDates({ endDate, params, startDate, today, tonight });
6✔
317
    params = Object.entries({ search, type }).reduce(paramReducer, params);
6✔
318
    pageStateQuery = formatPageState(pageState[selectedState], { defaults });
6✔
319
  } else if (selectedState === DEPLOYMENT_ROUTES.scheduled.key) {
18✔
320
    pageStateQuery = formatPageState(pageState[selectedState], { defaults });
2✔
321
  } else {
322
    pageStateQuery = formatActiveDeployments(pageState, { defaults });
16✔
323
  }
324
  return [pageStateQuery, params.toString()].filter(i => i).join('&');
48✔
325
};
326

327
const deploymentsPath = 'deployments/';
25✔
328
const parseDeploymentsPath = path => {
25✔
329
  const parts = path.split(deploymentsPath);
32✔
330
  if (parts.length > 1 && Object.keys(DEPLOYMENT_ROUTES).includes(parts[1])) {
32✔
331
    return parts[1];
21✔
332
  }
333
  return '';
11✔
334
};
335

336
const parseActiveDeployments = params =>
25✔
337
  [DEPLOYMENT_STATES.inprogress, DEPLOYMENT_STATES.pending].reduce((accu, state) => {
24✔
338
    if (!params.has(state)) {
48✔
339
      return accu;
18✔
340
    }
341
    const items = params.get(state).split(SEPARATOR);
30✔
342
    accu[state] = ['page', 'perPage'].reduce((stateAccu, key, index) => (items[index] ? { ...stateAccu, [key]: Number(items[index]) } : stateAccu), {});
60✔
343
    return accu;
30✔
344
  }, {});
345

346
const deploymentFields = {
25✔
347
  deviceId: { attribute: 'devices', parse: id => ({ id }), select: i => i },
1✔
348
  release: { attribute: 'release', parse: String, select: defaultSelector }
349
};
350

351
export const parseDeploymentsQuery = (params, { pageState, location, today, tonight }) => {
25✔
352
  const { endDate, startDate } = parseDateParams(params, today, tonight);
32✔
353
  const deploymentObject = Object.entries(deploymentFields).reduce(
32✔
354
    (accu, [key, { attribute, parse, select }]) => (params.has(key) ? { ...accu, [attribute]: select(params.getAll(key).map(parse)) } : accu),
64✔
355
    {}
356
  );
357
  const { state: selectedState, id, open, ...remainingPageState } = pageState;
32✔
358
  const tab = parseDeploymentsPath(location.pathname);
32✔
359
  const deploymentsTab = tab || selectedState || DEPLOYMENT_ROUTES.active.key;
32✔
360

361
  let state = {
32✔
362
    deploymentObject,
363
    general: {
364
      showCreationDialog: Boolean(open && !id),
37✔
365
      showReportDialog: Boolean(open && id),
37✔
366
      state: deploymentsTab
367
    }
368
  };
369
  if (deploymentsTab === DEPLOYMENT_ROUTES.finished.key) {
32✔
370
    const type = DEPLOYMENT_TYPES[params.get('type')] || '';
7✔
371
    const search = params.get('search') || '';
7✔
372
    state[deploymentsTab] = { ...remainingPageState, endDate, search, startDate, type };
7✔
373
  } else if (deploymentsTab === DEPLOYMENT_ROUTES.scheduled.key) {
25✔
374
    state[deploymentsTab] = { ...remainingPageState };
1✔
375
  } else {
376
    state = {
24✔
377
      ...state,
378
      ...parseActiveDeployments(params)
379
    };
380
  }
381
  return state;
32✔
382
};
383

384
export const generateDeploymentsPath = ({ pageState }) => {
25✔
385
  const { state: selectedState = DEPLOYMENT_ROUTES.active.key } = pageState.general;
24!
386
  return `/deployments/${selectedState}`;
24✔
387
};
388

389
const releasesRoot = '/releases';
25✔
390
export const formatReleases = ({ pageState: { selectedTags = [], tab } }) => {
25!
391
  const formattedFilters = selectedTags.map(tag => `tag=${tag}`);
8✔
392
  if (tab) {
8✔
393
    formattedFilters.push(`tab=${tab}`);
2✔
394
  }
395
  return formattedFilters.join('&');
8✔
396
};
397
export const generateReleasesPath = ({ pageState: { selectedRelease } }) => `${releasesRoot}${selectedRelease ? `/${selectedRelease}` : ''}`;
25✔
398

399
export const parseReleasesQuery = (queryParams, extraProps) => {
25✔
400
  const tab = queryParams.has('tab') ? queryParams.get('tab') : undefined;
9✔
401
  const tags = queryParams.has('tag') ? queryParams.getAll('tag') : [];
9✔
402
  let selectedRelease = extraProps.location.pathname.substring(releasesRoot.length + 1);
9✔
403
  if (!selectedRelease && extraProps.pageState.id?.length) {
9!
404
    selectedRelease = extraProps.pageState.id[0];
×
405
  }
406
  return { selectedRelease, tab, tags };
9✔
407
};
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