Persist and restore selected project and environment across page reloads
- Add refreshOrganization/refreshProject/refreshEnvironment to context store — update value and localStorage without clearing child selections - OrgSelector: use refreshOrganization when same org is already stored, preventing the chain that was clearing project and environment - ProjectSelector: watch org ID (not object ref) so refresh doesn't retrigger; add autoSelectProject to restore stored project or auto-select single project - EnvironmentTabBar: watch project ID; skip env-clear on initial mount; add autoSelectEnvironment to restore stored env or fall back to first - Fix test fixtures missing isPrimary field; update EnvironmentTabBar tests to match v-select implementation; add tests for restore behavior Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -56,13 +56,6 @@ describe('EnvironmentTabBar', () => {
|
||||
expect(envApi.listEnvironments).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('ThenNoProjectMessageIsShown', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="env-no-project-message"]').exists()).toBe(true);
|
||||
});
|
||||
|
||||
it('ThenCreateEnvironmentButtonIsDisabled', async () => {
|
||||
const wrapper = mountComponent();
|
||||
await flushPromises();
|
||||
@@ -84,7 +77,7 @@ describe('EnvironmentTabBar', () => {
|
||||
expect(envApi.listEnvironments).toHaveBeenCalledWith(mockProject.id);
|
||||
});
|
||||
|
||||
it('ThenEnvironmentChipsAreRendered', async () => {
|
||||
it('ThenEnvironmentSelectIsEnabled', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
@@ -93,8 +86,7 @@ describe('EnvironmentTabBar', () => {
|
||||
contextStore.setProject(mockProject);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="env-chip-development"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="env-chip-production"]').exists()).toBe(true);
|
||||
expect(wrapper.find('[data-testid="env-select"]').attributes('disabled')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('ThenFirstEnvironmentIsAutoSelected', async () => {
|
||||
@@ -122,7 +114,7 @@ describe('EnvironmentTabBar', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenEnvironmentChipIsClicked', () => {
|
||||
describe('WhenEnvironmentIsSelected', () => {
|
||||
it('ThenContextStoreIsUpdatedWithSelectedEnvironment', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
@@ -132,7 +124,7 @@ describe('EnvironmentTabBar', () => {
|
||||
contextStore.setProject(mockProject);
|
||||
await flushPromises();
|
||||
|
||||
await wrapper.findComponent({ name: 'VChipGroup' }).vm.$emit('update:modelValue', 1);
|
||||
await wrapper.findComponent({ name: 'VSelect' }).vm.$emit('update:modelValue', mockEnvironments[1]);
|
||||
|
||||
expect(contextStore.currentEnvironment).toEqual(mockEnvironments[1]);
|
||||
});
|
||||
@@ -158,16 +150,49 @@ describe('EnvironmentTabBar', () => {
|
||||
});
|
||||
|
||||
describe('WhenApiReturnsNoEnvironments', () => {
|
||||
it('ThenEmptyMessageIsDisplayed', async () => {
|
||||
it('ThenNoEnvironmentIsAutoSelected', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
const wrapper = mountComponent();
|
||||
mountComponent();
|
||||
|
||||
contextStore.setProject(mockProject);
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.find('[data-testid="env-empty-message"]').exists()).toBe(true);
|
||||
expect(contextStore.currentEnvironment).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenStoredEnvironmentExistsInContextStore', () => {
|
||||
it('ThenStoredEnvironmentIsRestoredAfterFetch', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnvironments[1]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentEnvironment).toEqual(mockEnvironments[1]);
|
||||
});
|
||||
|
||||
it('ThenEnvironmentIsClearedWhenProjectChanges', async () => {
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue(mockEnvironments);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnvironments[0]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const newProject: ProjectResponse = { ...mockProject, id: 20, name: 'Mobile' };
|
||||
jest.mocked(envApi.listEnvironments).mockResolvedValue([]);
|
||||
contextStore.setProject(newProject);
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentEnvironment).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -18,8 +18,8 @@ import * as orgsApi from '@/api/organizations';
|
||||
const dialogStub = { template: '<div><slot /></div>' };
|
||||
|
||||
const mockOrgs: OrganizationResponse[] = [
|
||||
{ id: 1, name: 'Acme Corp', createdAt: '2026-01-01T00:00:00Z' },
|
||||
{ id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z' },
|
||||
{ id: 1, name: 'Acme Corp', createdAt: '2026-01-01T00:00:00Z', isPrimary: false },
|
||||
{ id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z', isPrimary: false },
|
||||
];
|
||||
|
||||
function mountComponent() {
|
||||
@@ -132,7 +132,7 @@ describe('OrgSelector', () => {
|
||||
});
|
||||
|
||||
describe('WhenCreateOrganizationIsConfirmed', () => {
|
||||
const newOrg: OrganizationResponse = { id: 3, name: 'New Org', createdAt: '2026-04-13T00:00:00Z' };
|
||||
const newOrg: OrganizationResponse = { id: 3, name: 'New Org', createdAt: '2026-04-13T00:00:00Z', isPrimary: false };
|
||||
|
||||
it('ThenCreateOrganizationApiIsCalled', async () => {
|
||||
jest.mocked(orgsApi.listOrganizations).mockResolvedValue([]);
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as projectsApi from '@/api/projects';
|
||||
|
||||
const dialogStub = { template: '<div><slot /></div>' };
|
||||
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z' };
|
||||
const mockOrg: OrganizationResponse = { id: 1, name: 'Acme', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
const mockProjects: ProjectResponse[] = [
|
||||
{ id: 10, name: 'Website', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' },
|
||||
{ id: 11, name: 'Mobile App', organizationId: 1, hideDisabledFlags: false, createdAt: '2026-01-01T00:00:00Z' },
|
||||
@@ -104,6 +104,54 @@ describe('ProjectSelector', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenStoredProjectExistsInContextStore', () => {
|
||||
it('ThenStoredProjectIsRestoredAfterFetch', async () => {
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue(mockProjects);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization(mockOrg);
|
||||
contextStore.setProject(mockProjects[1]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentProject).toEqual(mockProjects[1]);
|
||||
});
|
||||
|
||||
it('ThenProjectIsClearedWhenNotFoundInNewOrg', async () => {
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue(mockProjects);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization(mockOrg);
|
||||
contextStore.setProject(mockProjects[0]);
|
||||
|
||||
mountComponent();
|
||||
await flushPromises();
|
||||
|
||||
const newOrg: OrganizationResponse = { id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue([]);
|
||||
contextStore.setOrganization(newOrg);
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentProject).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenSingleProjectIsAvailable', () => {
|
||||
it('ThenItIsAutoSelected', async () => {
|
||||
const singleProject = mockProjects[0];
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue([singleProject]);
|
||||
|
||||
const contextStore = useContextStore();
|
||||
mountComponent();
|
||||
|
||||
contextStore.setOrganization(mockOrg);
|
||||
await flushPromises();
|
||||
|
||||
expect(contextStore.currentProject).toEqual(singleProject);
|
||||
});
|
||||
});
|
||||
|
||||
describe('WhenOrganizationChanges', () => {
|
||||
it('ThenProjectsAreReloaded', async () => {
|
||||
jest.mocked(projectsApi.listProjects).mockResolvedValue(mockProjects);
|
||||
@@ -114,7 +162,7 @@ describe('ProjectSelector', () => {
|
||||
contextStore.setOrganization(mockOrg);
|
||||
await flushPromises();
|
||||
|
||||
const newOrg: OrganizationResponse = { id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z' };
|
||||
const newOrg: OrganizationResponse = { id: 2, name: 'Globex', createdAt: '2026-01-01T00:00:00Z', isPrimary: false };
|
||||
contextStore.setOrganization(newOrg);
|
||||
await flushPromises();
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('FeatureDetail', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment({ id: 100, name: 'Development', apiKey: 'env-dev', projectId: 10, createdAt: '' });
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('FeatureDialog', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
});
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('TagsChipInput', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
});
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@ describe('IdentityDetail', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
contextStore.setEnvironment(mockEnv);
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ describe('SegmentEditor', () => {
|
||||
setActivePinia(createPinia());
|
||||
jest.clearAllMocks();
|
||||
const contextStore = useContextStore();
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '' });
|
||||
contextStore.setOrganization({ id: 1, name: 'Acme', createdAt: '', isPrimary: false });
|
||||
contextStore.setProject(mockProject);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user