Adding Segments
This commit is contained in:
148
src/admin/tests/unit/components/segments/SegmentEditor.spec.ts
Normal file
148
src/admin/tests/unit/components/segments/SegmentEditor.spec.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils';
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { createRouter, createMemoryHistory } from 'vue-router';
|
||||
import SegmentEditor from '@/components/segments/SegmentEditor.vue';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import type { ProjectResponse, SegmentResponse } from '@/types/api';
|
||||
|
||||
jest.mock('@/api/segments');
|
||||
jest.mock('@/api/client', () => ({
|
||||
setAuthTokens: jest.fn(),
|
||||
clearAuthTokens: jest.fn(),
|
||||
getAccessToken: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as segmentsApi from '@/api/segments';
|
||||
|
||||
const mockProject: ProjectResponse = {
|
||||
id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const mockSegment: SegmentResponse = {
|
||||
id: 1,
|
||||
name: 'beta_users',
|
||||
projectId: 10,
|
||||
createdAt: '2026-01-01T00:00:00Z',
|
||||
rules: [{ type: 'AND', conditions: [{ property: 'plan', operator: 'Equal', value: 'beta' }] }],
|
||||
};
|
||||
|
||||
const dialogStub = { template: '<div><slot /></div>' };
|
||||
|
||||
function mountEditor(props: Record<string, unknown> = {}) {
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '<div />' } }] });
|
||||
return mount(SegmentEditor, {
|
||||
props: { modelValue: true, segment: null, ...props },
|
||||
global: { plugins: [router], stubs: { 'v-dialog': dialogStub } },
|
||||
});
|
||||
}
|
||||
|
||||
describe('SegmentEditor', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
});
|
||||
|
||||
describe('WhenOpenedForCreate', () => {
|
||||
it('ThenEditorIsRendered', () => {
|
||||
const wrapper = mountEditor();
|
||||
expect(wrapper.find('[data-testid="segment-editor"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenTitleShowsCreateSegment', () => {
|
||||
const wrapper = mountEditor();
|
||||
expect(wrapper.text()).toContain('Create Segment');
|
||||
});
|
||||
|
||||
it('ThenNameInputIsEmpty', () => {
|
||||
const wrapper = mountEditor();
|
||||
const nameInput = wrapper.find('[data-testid="segment-name-input"]');
|
||||
expect(nameInput.exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenNoRulesMessageIsShown', () => {
|
||||
const wrapper = mountEditor();
|
||||
expect(wrapper.find('[data-testid="no-rules-message"]').exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenOpenedForEdit', () => {
|
||||
it('ThenTitleShowsEditSegment', () => {
|
||||
const wrapper = mountEditor({ segment: mockSegment });
|
||||
expect(wrapper.text()).toContain('Edit Segment');
|
||||
});
|
||||
|
||||
it('ThenFormIsPrePopulatedWithSegmentData', () => {
|
||||
const wrapper = mountEditor({ segment: mockSegment });
|
||||
const nameInput = wrapper.findComponent('[data-testid="segment-name-input"]') as VueWrapper<any>;
|
||||
expect(nameInput.props('modelValue')).toBe(mockSegment.name);
|
||||
});
|
||||
|
||||
it('ThenExistingRulesAreRendered', () => {
|
||||
const wrapper = mountEditor({ segment: mockSegment });
|
||||
expect(wrapper.findComponent({ name: 'RuleGroupEditor' }).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenAddRuleGroupButtonIsClicked', () => {
|
||||
it('ThenARuleGroupEditorIsAdded', async () => {
|
||||
const wrapper = mountEditor();
|
||||
|
||||
await wrapper.find('[data-testid="add-rule-group-btn"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.findComponent({ name: 'RuleGroupEditor' }).exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenSaveIsClickedWithValidData', () => {
|
||||
it('ThenCreateSegmentIsCalledForNewSegment', async () => {
|
||||
const created: SegmentResponse = { id: 99, name: 'new_seg', projectId: 10, createdAt: '2026-01-01T00:00:00Z', rules: [] };
|
||||
jest.mocked(segmentsApi.createSegment).mockResolvedValue(created);
|
||||
|
||||
const wrapper = mountEditor();
|
||||
|
||||
await (wrapper.findComponent('[data-testid="segment-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'new_seg');
|
||||
await wrapper.find('[data-testid="save-segment-btn"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(segmentsApi.createSegment).toHaveBeenCalledWith(
|
||||
mockProject.id,
|
||||
expect.objectContaining({ name: 'new_seg' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('ThenUpdateSegmentIsCalledForExistingSegment', async () => {
|
||||
const updated = { ...mockSegment, name: 'updated_name' };
|
||||
jest.mocked(segmentsApi.updateSegment).mockResolvedValue(updated);
|
||||
|
||||
const wrapper = mountEditor({ segment: mockSegment });
|
||||
|
||||
await wrapper.find('[data-testid="save-segment-btn"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(segmentsApi.updateSegment).toHaveBeenCalledWith(
|
||||
mockProject.id,
|
||||
mockSegment.id,
|
||||
expect.objectContaining({ name: mockSegment.name }),
|
||||
);
|
||||
});
|
||||
|
||||
it('ThenSavedEventIsEmitted', async () => {
|
||||
const created: SegmentResponse = { id: 99, name: 'new_seg', projectId: 10, createdAt: '2026-01-01T00:00:00Z', rules: [] };
|
||||
jest.mocked(segmentsApi.createSegment).mockResolvedValue(created);
|
||||
|
||||
const wrapper = mountEditor();
|
||||
|
||||
await (wrapper.findComponent('[data-testid="segment-name-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'new_seg');
|
||||
await wrapper.find('[data-testid="save-segment-btn"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.emitted('saved')).toBeTruthy();
|
||||
expect(wrapper.emitted('saved')![0]).toEqual([created]);
|
||||
});
|
||||
});
|
||||
});
|
||||
232
src/admin/tests/unit/views/SegmentsView.spec.ts
Normal file
232
src/admin/tests/unit/views/SegmentsView.spec.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { describe, it, expect, beforeEach, jest } from '@jest/globals';
|
||||
import { mount, flushPromises, type VueWrapper } from '@vue/test-utils';
|
||||
import { setActivePinia, createPinia } from 'pinia';
|
||||
import { createRouter, createMemoryHistory } from 'vue-router';
|
||||
import SegmentsView from '@/views/SegmentsView.vue';
|
||||
import { useContextStore } from '@/stores/context';
|
||||
import type { ProjectResponse, SegmentResponse } from '@/types/api';
|
||||
|
||||
jest.mock('@/api/segments');
|
||||
jest.mock('@/api/client', () => ({
|
||||
setAuthTokens: jest.fn(),
|
||||
clearAuthTokens: jest.fn(),
|
||||
getAccessToken: jest.fn(),
|
||||
}));
|
||||
|
||||
import * as segmentsApi from '@/api/segments';
|
||||
|
||||
const mockProject: ProjectResponse = {
|
||||
id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z',
|
||||
};
|
||||
|
||||
const dialogStub = { template: '<div><slot /></div>' };
|
||||
|
||||
const mockSegments: SegmentResponse[] = [
|
||||
{
|
||||
id: 1, name: 'beta_users', projectId: 10, createdAt: '2026-01-01T00:00:00Z',
|
||||
rules: [{ type: 'AND', conditions: [{ property: 'plan', operator: 'Equal', value: 'beta' }] }],
|
||||
},
|
||||
{
|
||||
id: 2, name: 'power_users', projectId: 10, createdAt: '2026-01-01T00:00:00Z',
|
||||
rules: [],
|
||||
},
|
||||
];
|
||||
|
||||
function mountView() {
|
||||
const router = createRouter({ history: createMemoryHistory(), routes: [{ path: '/', component: { template: '<div />' } }] });
|
||||
return mount(SegmentsView, { global: { plugins: [router], stubs: { 'v-dialog': dialogStub } } });
|
||||
}
|
||||
|
||||
describe('SegmentsView', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('WhenNoProjectIsSelected', () => {
|
||||
it('ThenEmptyStateIsShown', () => {
|
||||
const wrapper = mountView();
|
||||
expect(wrapper.find('[data-testid="segments-table"]').exists()).toBe(false);
|
||||
expect(wrapper.text()).toContain('Select a project');
|
||||
});
|
||||
|
||||
it('ThenCreateButtonIsNotVisible', () => {
|
||||
const wrapper = mountView();
|
||||
expect(wrapper.find('[data-testid="create-segment-btn"]').exists()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenProjectIsSelected', () => {
|
||||
it('ThenSegmentsAreLoaded', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
mountView();
|
||||
await flushPromises();
|
||||
|
||||
expect(segmentsApi.listSegments).toHaveBeenCalledWith(mockProject.id);
|
||||
});
|
||||
|
||||
it('ThenSegmentsTableIsRendered', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="segments-table"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="create-segment-btn"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenSegmentNamesAreDisplayed', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('beta_users');
|
||||
expect(wrapper.text()).toContain('power_users');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenCreateButtonIsClicked', () => {
|
||||
it('ThenSegmentEditorIsOpened', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="create-segment-btn"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const editor = wrapper.findComponent({ name: 'SegmentEditor' });
|
||||
expect(editor.exists()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenEditButtonIsClicked', () => {
|
||||
it('ThenSegmentEditorIsOpenedWithSegment', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue(mockSegments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="edit-segment-1"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
const editor = wrapper.findComponent({ name: 'SegmentEditor' });
|
||||
expect(editor.props('segment')).toEqual(mockSegments[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenDeleteConfirmationIsCompleted', () => {
|
||||
it('ThenDeleteSegmentIsCalledWhenNameMatches', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([...mockSegments]);
|
||||
jest.mocked(segmentsApi.deleteSegment).mockResolvedValue(undefined);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="delete-segment-1"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.find('[data-testid="delete-confirm-dialog"]').exists()).toBe(true);
|
||||
|
||||
// Confirm button should be disabled before typing the name
|
||||
expect(wrapper.find('[data-testid="confirm-delete-btn"]').attributes('disabled')).toBeDefined();
|
||||
|
||||
// Type segment name to enable button
|
||||
await (wrapper.findComponent('[data-testid="delete-confirm-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'beta_users');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await wrapper.find('[data-testid="confirm-delete-btn"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(segmentsApi.deleteSegment).toHaveBeenCalledWith(mockProject.id, 1);
|
||||
});
|
||||
|
||||
it('ThenSegmentIsRemovedFromListAfterDeletion', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([...mockSegments]);
|
||||
jest.mocked(segmentsApi.deleteSegment).mockResolvedValue(undefined);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.find('[data-testid="delete-segment-1"]').trigger('click');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await (wrapper.findComponent('[data-testid="delete-confirm-input"]') as VueWrapper<any>).vm.$emit('update:modelValue', 'beta_users');
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
await wrapper.find('[data-testid="confirm-delete-btn"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).not.toContain('beta_users');
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenSegmentIsSaved', () => {
|
||||
it('ThenNewSegmentIsAddedToList', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
const newSegment: SegmentResponse = { id: 99, name: 'new_segment', projectId: 10, createdAt: '2026-01-01T00:00:00Z', rules: [] };
|
||||
|
||||
await wrapper.findComponent({ name: 'SegmentEditor' }).vm.$emit('saved', newSegment);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('new_segment');
|
||||
});
|
||||
|
||||
it('ThenExistingSegmentIsUpdatedInList', async () => {
|
||||
jest.mocked(segmentsApi.listSegments).mockResolvedValue([...mockSegments]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setProject(mockProject);
|
||||
|
||||
const wrapper = mountView();
|
||||
await flushPromises();
|
||||
|
||||
const updated: SegmentResponse = { ...mockSegments[0], name: 'updated_segment' };
|
||||
|
||||
await wrapper.findComponent({ name: 'SegmentEditor' }).vm.$emit('saved', updated);
|
||||
await wrapper.vm.$nextTick();
|
||||
|
||||
expect(wrapper.text()).toContain('updated_segment');
|
||||
expect(wrapper.text()).not.toContain('beta_users');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user