mattermost-mobile-upstream/app/components/channel_attribute_editor/utils.test.ts
Pablo Vélez 4d842026f6
Some checks failed
ci / lint-typecheck (push) Has been cancelled
ci / detox-lint-typecheck (push) Has been cancelled
ci / test (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Mm 70564 channel labels use in channel creation (#10146)
* MM-70516 - Add ability to edit channel attribute values from Channel Info

* Add API methods for patching property fields and setting channel attribute requirements

* Update channel attribute clearing behavior for required and non-required fields

* MM-70564 - use channel attributes on channel creation

* Fix undefined propertyValues and ternary formatting in channel attributes

* Fix spelling of "colour" to "color" in attribute chip tests and related comments

* Add visibility checks for channel attribute labels in tests

* Fix data-integrity, permission, and accessibility issues in channel attributes: scoped pruning, atomic writes, race serialization, required clear/stale-option guards

* Fix handling of null values in apiGetChannelAttributeValue and update test assertions for channel attributes

* Improve channel attribute editor sizing for long labels, keep Save visible, and prevent keyboard overlap on Android

* Address CodeRabbit feedback for nullable values, bottom-sheet state, async validation, and reliable E2E cleanup

* lock-safe field fetch, create-time gate recheck, scoped value pruning, and tappable header chips

* Address CodeRabbit feedback: drop queued attribute saves on channel switch and use page object selectors in E2E

* Respect the ChannelAttributesRequired flag on channel creation and enable it for channel attribute E2E

* Skip required channel attribute E2E tests and stop enabling ChannelAttributesRequired on the shared server until  they move to SITE_3

* Fix clearing channel attributes on iOS by sending an empty value instead of null, and stop E2E suites from turning  off the ChannelAttributes flag mid-run

* Refactor getEmptyPropertyValue to accept string type for fieldType parameter

* E2E: give channel attribute test fields a suite-unique suffix

Pool shards may only create optional property fields named <name>_<6 hex>; fixed names were blocked by the pool guard.

* E2E: restore channel attribute cleanup checks; name network error codes

Brings back the PR's afterEach/afterAll error handling that the main merge replaced, and makes a no-response error show its codes (an AggregateError has an empty message).

* E2E: run channel attribute creation and editing specs on server B

Their 11 required-attribute tests could not run on the shared servers. Both specs now run on the mutation server with ChannelAttributesRequired on for their run, and the tests are re-enabled. setChannelAttributesRequired moves to the shared helper.

* Check header chips instead of their wrapper and use toExist for the create screen in channel attribute E2E

---------

Co-authored-by: yasserfaraazkhan <attitude3cena.yf@gmail.com>
2026-10-30 15:03:55 +01:00

132 lines
6.6 KiB
TypeScript

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getChannelAttributeEditorSnapPoints, SHEET_MAX_ROWS} from './utils';
// Mirrors the one mutation GenericBottomSheetRoute performs on whatever this
// helper returns, so ordering can be asserted the way it will actually be
// consumed rather than on the helper's raw output.
function applyRouteMargin(points: number[], margin: number): number[] {
const withMargin = [...points];
if (typeof withMargin[1] === 'number') {
withMargin[1] += margin;
}
return withMargin;
}
describe('getChannelAttributeEditorSnapPoints', () => {
const base = {
bottomInset: 0,
isTablet: false,
windowHeight: 800,
};
it('should return two ascending points ordered bottom to top', () => {
const [closed, open] = getChannelAttributeEditorSnapPoints({...base, fieldType: 'select', totalRows: 3});
expect(closed).toBe(1);
expect(open).toBeGreaterThan(closed);
});
it('should never resolve the open point above the window-height cap', () => {
const [, open] = getChannelAttributeEditorSnapPoints({...base, fieldType: 'select', totalRows: 3});
expect(open).toBeLessThanOrEqual(base.windowHeight * 0.8);
});
it('should never resolve the effective, post-route-margin open point above the window-height cap', () => {
const bottomInset = 34;
const points = getChannelAttributeEditorSnapPoints({...base, bottomInset, fieldType: 'select', totalRows: 3});
const [, effectiveOpen] = applyRouteMargin(points, bottomInset);
expect(effectiveOpen).toBeLessThanOrEqual(base.windowHeight * 0.8);
});
it('should cap the collapsed point on a short window instead of exceeding it', () => {
const [, open] = getChannelAttributeEditorSnapPoints({...base, windowHeight: 320, fieldType: 'select', totalRows: 3});
expect(open).toBeLessThanOrEqual(320 * 0.8);
});
it('should add a taller third point for an option field only when there are more rows than the collapsed estimate covers', () => {
const withinLimit = getChannelAttributeEditorSnapPoints({...base, fieldType: 'multiselect', totalRows: SHEET_MAX_ROWS});
const overLimit = getChannelAttributeEditorSnapPoints({...base, fieldType: 'multiselect', totalRows: SHEET_MAX_ROWS + 10});
expect(withinLimit).toHaveLength(2);
expect(overLimit).toHaveLength(3);
});
it('should keep the three points strictly ascending when a taller point is offered', () => {
const points = getChannelAttributeEditorSnapPoints({...base, fieldType: 'multiselect', totalRows: SHEET_MAX_ROWS + 10});
expect(points[0]).toBeLessThan(points[1]);
expect(points[1]).toBeLessThan(points[2]);
});
it('should never offer a taller point for a text field: it never has more than one row, and the caller opens it with keyboardBehavior="interactive" instead of relying on a taller snap point for the keyboard', () => {
const points = getChannelAttributeEditorSnapPoints({...base, fieldType: 'text', totalRows: 1});
expect(points).toHaveLength(2);
});
it('should stay ordered after GenericBottomSheetRoute applies its margin, for every window height that offers a third point', () => {
// Scans rather than hand-picking one windowHeight: what matters is that
// no window size in range ever produces an inversion once the route's
// margin lands on the second point, not that one particular value does.
let sawThreePoints = false;
for (let windowHeight = 300; windowHeight <= 1200; windowHeight += 10) {
const points = getChannelAttributeEditorSnapPoints({...base, windowHeight, bottomInset: 34, fieldType: 'multiselect', totalRows: SHEET_MAX_ROWS + 10});
if (points.length < 3) {
continue;
}
sawThreePoints = true;
const afterRoute = applyRouteMargin(points, 34);
expect(afterRoute[1]).toBeLessThan(afterRoute[2]);
}
expect(sawThreePoints).toBe(true);
});
it('should drop the taller point rather than risk an inversion when the collapsed estimate lands within the route margin of the cap', () => {
// Scans for a windowHeight where the collapsed point ends up close
// enough to the cap that adding the route's margin would invert it, and
// asserts the helper falls back to two points there instead.
let sawDroppedPoint = false;
for (let windowHeight = 300; windowHeight <= 1200; windowHeight += 10) {
const uncapped = getChannelAttributeEditorSnapPoints({...base, windowHeight, bottomInset: 0, fieldType: 'multiselect', totalRows: SHEET_MAX_ROWS + 10});
const points = getChannelAttributeEditorSnapPoints({...base, windowHeight, bottomInset: 34, fieldType: 'multiselect', totalRows: SHEET_MAX_ROWS + 10});
if (uncapped.length === 3 && points.length === 2) {
sawDroppedPoint = true;
}
}
expect(sawDroppedPoint).toBe(true);
});
it('should reserve extra height for the save button on text and multiselect fields but not select', () => {
const selectPoints = getChannelAttributeEditorSnapPoints({...base, fieldType: 'select', totalRows: 1});
const multiselectPoints = getChannelAttributeEditorSnapPoints({...base, fieldType: 'multiselect', totalRows: 1});
expect(multiselectPoints[1]).toBeGreaterThan(selectPoints[1]);
});
it('should account for the safe-area inset only when a save button is shown', () => {
const withoutInset = getChannelAttributeEditorSnapPoints({...base, fieldType: 'multiselect', totalRows: 1});
const withInset = getChannelAttributeEditorSnapPoints({...base, fieldType: 'multiselect', totalRows: 1, bottomInset: 34});
const selectWithoutInset = getChannelAttributeEditorSnapPoints({...base, fieldType: 'select', totalRows: 1});
const selectWithInset = getChannelAttributeEditorSnapPoints({...base, fieldType: 'select', totalRows: 1, bottomInset: 34});
expect(withInset[1]).toBe(withoutInset[1] + 34);
expect(selectWithInset[1]).toBe(selectWithoutInset[1]);
});
it('should reserve more separator margin on tablet than on phone', () => {
const phone = getChannelAttributeEditorSnapPoints({...base, fieldType: 'select', totalRows: 1, isTablet: false});
const tablet = getChannelAttributeEditorSnapPoints({...base, fieldType: 'select', totalRows: 1, isTablet: true});
expect(tablet[1]).toBeGreaterThan(phone[1]);
});
});