UX tweaks, traits bug fix, adding member info
This commit is contained in:
@@ -4,10 +4,11 @@ import { setActivePinia, createPinia } from 'pinia';
|
||||
import { createRouter, createMemoryHistory } from 'vue-router';
|
||||
import IdentityDetail from '@/components/identities/IdentityDetail.vue';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import type { EnvironmentResponse, IdentityResponse, FeatureResponse, FeatureStateResponse, ProjectResponse } from '@/types/api';
|
||||
import type { EnvironmentResponse, IdentityResponse, FeatureResponse, FeatureStateResponse, ProjectResponse, SegmentSummaryResponse } from '@/types/api';
|
||||
|
||||
jest.mock('@/api/identities');
|
||||
jest.mock('@/api/features');
|
||||
jest.mock('@/api/featureSegments');
|
||||
jest.mock('@/api/client', () => ({
|
||||
setAuthTokens: jest.fn(),
|
||||
clearAuthTokens: jest.fn(),
|
||||
@@ -16,6 +17,7 @@ jest.mock('@/api/client', () => ({
|
||||
|
||||
import * as identitiesApi from '@/api/identities';
|
||||
import * as featuresApi from '@/api/features';
|
||||
import * as featureSegmentsApi from '@/api/featureSegments';
|
||||
|
||||
const mockProject: ProjectResponse = {
|
||||
id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z',
|
||||
@@ -38,6 +40,10 @@ const mockOverride: FeatureStateResponse = {
|
||||
id: 500, featureId: 10, environmentId: 100, identityId: 1, featureSegmentId: null,
|
||||
enabled: true, value: null, createdAt: '2026-01-01T00:00:00Z', updatedAt: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
const mockSegments: SegmentSummaryResponse[] = [
|
||||
{ id: 1, name: 'Pro Users' },
|
||||
{ id: 2, name: 'US Customers' },
|
||||
];
|
||||
|
||||
const dialogStub = { template: '<div><slot /></div>' };
|
||||
|
||||
@@ -60,6 +66,7 @@ describe('IdentityDetail', () => {
|
||||
|
||||
jest.mocked(identitiesApi.getIdentityFeatureStates).mockResolvedValue([]);
|
||||
jest.mocked(featuresApi.listFeatures).mockResolvedValue({ count: 2, next: null, previous: null, results: mockFeatures });
|
||||
jest.mocked(featureSegmentsApi.listIdentitySegments).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe('WhenOpenedWithIdentity', () => {
|
||||
@@ -75,11 +82,12 @@ describe('IdentityDetail', () => {
|
||||
expect(wrapper.text()).toContain('user-alice');
|
||||
});
|
||||
|
||||
it('ThenTraitsAndOverridesTabsArePresent', async () => {
|
||||
it('ThenTraitsOverridesAndSegmentsTabsArePresent', async () => {
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
expect(wrapper.find('[data-testid="tab-traits"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="tab-overrides"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="tab-segments"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -96,14 +104,129 @@ describe('IdentityDetail', () => {
|
||||
expect(wrapper.find('[data-testid="trait-row-plan"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="trait-row-country"]').exists()).toBe(true);
|
||||
expect(wrapper.text()).toContain('plan');
|
||||
expect(wrapper.text()).toContain('pro');
|
||||
});
|
||||
|
||||
it('ThenEmptyMessageIsShownWhenNoTraits', async () => {
|
||||
it('ThenNoTraitsMessageIsShownWhenNoTraits', async () => {
|
||||
const identityWithNoTraits: IdentityResponse = { ...mockIdentity, traits: [] };
|
||||
const wrapper = mountDetail({ identity: identityWithNoTraits });
|
||||
await flushPromises();
|
||||
expect(wrapper.find('[data-testid="traits-table"]').exists()).toBe(false);
|
||||
expect(wrapper.find('[data-testid="no-traits-message"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenAddTraitButtonIsPresent', async () => {
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
expect(wrapper.find('[data-testid="add-trait-btn"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenAddTraitButtonIsDisabledWhenKeyIsEmpty', async () => {
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
const btn = wrapper.find('[data-testid="add-trait-btn"]');
|
||||
expect(btn.attributes('disabled')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenAddTraitIsSubmitted', () => {
|
||||
it('ThenUpsertIdentityTraitIsCalledWithKeyAndValue', async () => {
|
||||
jest.mocked(identitiesApi.upsertIdentityTrait).mockResolvedValue({ key: 'role', value: 'admin' });
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
(wrapper.vm as any).newTraitKey = 'role';
|
||||
(wrapper.vm as any).newTraitValue = 'admin';
|
||||
await (wrapper.vm as any).onAddTrait();
|
||||
|
||||
expect(identitiesApi.upsertIdentityTrait).toHaveBeenCalledWith(
|
||||
mockEnv.apiKey, mockIdentity.id, 'role', { value: 'admin' },
|
||||
);
|
||||
});
|
||||
|
||||
it('ThenNewTraitAppearsInTable', async () => {
|
||||
jest.mocked(identitiesApi.upsertIdentityTrait).mockResolvedValue({ key: 'role', value: 'admin' });
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
(wrapper.vm as any).newTraitKey = 'role';
|
||||
(wrapper.vm as any).newTraitValue = 'admin';
|
||||
await (wrapper.vm as any).onAddTrait();
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="trait-row-role"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenInputsAreClearedAfterSuccess', async () => {
|
||||
jest.mocked(identitiesApi.upsertIdentityTrait).mockResolvedValue({ key: 'role', value: 'admin' });
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
(wrapper.vm as any).newTraitKey = 'role';
|
||||
(wrapper.vm as any).newTraitValue = 'admin';
|
||||
await (wrapper.vm as any).onAddTrait();
|
||||
|
||||
expect((wrapper.vm as any).newTraitKey).toBe('');
|
||||
expect((wrapper.vm as any).newTraitValue).toBe('');
|
||||
});
|
||||
|
||||
it('ThenErrorIsShownWhenApiFails', async () => {
|
||||
jest.mocked(identitiesApi.upsertIdentityTrait).mockRejectedValue(new Error('fail'));
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
(wrapper.vm as any).newTraitKey = 'role';
|
||||
await (wrapper.vm as any).onAddTrait();
|
||||
|
||||
expect(wrapper.text()).toContain('Failed to add trait');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenTraitValueIsSaved', () => {
|
||||
it('ThenUpsertIdentityTraitIsCalledWithUpdatedValue', async () => {
|
||||
jest.mocked(identitiesApi.upsertIdentityTrait).mockResolvedValue({ key: 'plan', value: 'enterprise' });
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
await (wrapper.vm as any).onSaveTrait('plan', 'enterprise');
|
||||
|
||||
expect(identitiesApi.upsertIdentityTrait).toHaveBeenCalledWith(
|
||||
mockEnv.apiKey, mockIdentity.id, 'plan', { value: 'enterprise' },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenDeleteTraitIsClicked', () => {
|
||||
it('ThenDeleteIdentityTraitIsCalledWithKey', async () => {
|
||||
jest.mocked(identitiesApi.deleteIdentityTrait).mockResolvedValue(undefined);
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="delete-trait-plan"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(identitiesApi.deleteIdentityTrait).toHaveBeenCalledWith(
|
||||
mockEnv.apiKey, mockIdentity.id, 'plan',
|
||||
);
|
||||
});
|
||||
|
||||
it('ThenTraitRowIsRemovedAfterDelete', async () => {
|
||||
jest.mocked(identitiesApi.deleteIdentityTrait).mockResolvedValue(undefined);
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="trait-row-plan"]').exists()).toBe(true);
|
||||
|
||||
await wrapper.find('[data-testid="delete-trait-plan"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="trait-row-plan"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -218,6 +341,64 @@ describe('IdentityDetail', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenSegmentsTabIsActive', () => {
|
||||
it('ThenListIdentitySegmentsIsCalledOnOpen', async () => {
|
||||
mountDetail();
|
||||
await flushPromises();
|
||||
expect(featureSegmentsApi.listIdentitySegments).toHaveBeenCalledWith(mockEnv.apiKey, mockIdentity.id);
|
||||
});
|
||||
|
||||
it('ThenNoSegmentsMessageIsShownWhenIdentityMatchesNone', async () => {
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="tab-segments"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="no-segments-message"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="segments-list"]').exists()).toBe(false);
|
||||
});
|
||||
|
||||
it('ThenSegmentChipsAreDisplayedWhenIdentityMatchesSegments', async () => {
|
||||
jest.mocked(featureSegmentsApi.listIdentitySegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="tab-segments"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="segments-list"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="segment-chip-1"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="segment-chip-2"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenSegmentNamesAreDisplayedInChips', async () => {
|
||||
jest.mocked(featureSegmentsApi.listIdentitySegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="tab-segments"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('Pro Users');
|
||||
expect(wrapper.text()).toContain('US Customers');
|
||||
});
|
||||
|
||||
it('ThenSegmentsErrorIsShownWhenApiFails', async () => {
|
||||
jest.mocked(featureSegmentsApi.listIdentitySegments).mockRejectedValue(new Error('Network error'));
|
||||
|
||||
const wrapper = mountDetail();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="tab-segments"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('Failed to load segments');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenAddOverrideIsSubmitted', () => {
|
||||
it('ThenSetIdentityFeatureStateIsCalledWithSelectedFeature', async () => {
|
||||
const newOverride: FeatureStateResponse = { ...mockOverride, featureId: 20, enabled: true };
|
||||
|
||||
@@ -104,7 +104,7 @@ describe('SettingsView', () => {
|
||||
describe('WhenInviteMemberIsClicked', () => {
|
||||
it('ThenInviteOrganizationMemberIsCalledWithUserId', async () => {
|
||||
jest.mocked(orgsApi.inviteOrganizationMember).mockResolvedValue(undefined);
|
||||
jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([{ userId: 42, role: 'User' }]);
|
||||
jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([{ userId: 42, firstName: 'Jane', lastName: 'Doe', email: 'jane@example.com', role: 'User', lastLoginAt: null }]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization(mockOrg);
|
||||
@@ -124,7 +124,7 @@ describe('SettingsView', () => {
|
||||
|
||||
describe('WhenRemoveMemberIsClicked', () => {
|
||||
it('ThenRemoveOrganizationMemberIsCalledAndMemberIsRemovedFromList', async () => {
|
||||
jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([{ userId: 5, role: 'User' }]);
|
||||
jest.mocked(orgsApi.listOrganizationMembers).mockResolvedValue([{ userId: 5, firstName: 'John', lastName: 'Smith', email: 'john@example.com', role: 'User', lastLoginAt: null }]);
|
||||
jest.mocked(orgsApi.removeOrganizationMember).mockResolvedValue(undefined);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
|
||||
Reference in New Issue
Block a user