Skip to content

Commit 58995df

Browse files
mrkaye97wsehl
authored andcommitted
Revert "Remove "account" dropdown', add notifications dropdown (#3365)" (#3664)
This reverts commit e71b6c0.
1 parent 7a0bdf9 commit 58995df

29 files changed

Lines changed: 683 additions & 791 deletions

frontend/app/cypress/e2e/auth/01-login.cy.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,16 @@ describe('auth: login', () => {
3333
}
3434
});
3535
cy.location('pathname', { timeout: 30000 }).should('include', '/tenants/');
36-
cy.get('[data-cy="v1-sidebar"]', { timeout: 30000 }).should('be.visible');
36+
cy.get('button[aria-label="User Menu"]').filter(':visible').first().click();
37+
// `data-cy="user-name"` exists in both the trigger and the dropdown content; scope to the open menu.
38+
cy.get('[role="menu"]')
39+
.filter(':visible')
40+
.first()
41+
.within(() => {
42+
cy.get('[data-cy="user-name"]')
43+
.filter(':visible')
44+
.first()
45+
.should('have.text', seededUsers.owner.name);
46+
});
3747
});
3848
});

frontend/app/cypress/e2e/auth/02-logout.cy.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@ describe('auth: logout', () => {
33
// Some environments don't have an "admin" seeded user; "owner" is sufficient to validate logout.
44
cy.login('owner');
55
cy.visit('/');
6-
cy.contains('button', 'Logout').filter(':visible').first().click();
6+
cy.get('button[aria-label="User Menu"]')
7+
.filter(':visible')
8+
.should('be.visible')
9+
.first()
10+
.click();
11+
// Menu item includes a keyboard shortcut, so match by substring.
12+
cy.contains('[role="menuitem"]', 'Log out').filter(':visible').click();
713
cy.location('pathname').should('include', '/auth/login');
814
});
915
});

frontend/app/cypress/e2e/auth/05-tenant-invite-accept.cy.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,12 @@ describe('Tenant Invite: accept', () => {
7878
});
7979
});
8080

81-
cy.contains('button', 'Logout').filter(':visible').first().click();
81+
cy.get('button[aria-label="User Menu"]')
82+
.filter(':visible')
83+
.should('be.visible')
84+
.first()
85+
.click();
86+
cy.contains('[role="menuitem"]', 'Log out').filter(':visible').click();
8287

8388
cy.location('pathname').should('include', '/auth/login');
8489
cy.get('input#email').type(seededUsers.member.email);
@@ -91,19 +96,6 @@ describe('Tenant Invite: accept', () => {
9196
.should('be.enabled')
9297
.click();
9398
});
94-
cy.location('pathname', { timeout: 30000 }).should(
95-
'match',
96-
/\/tenants\/.+/,
97-
);
98-
99-
// Open the notification dropdown and click the tenant invite notification
100-
cy.get('[data-cy="notifications-button"]', { timeout: 10000 })
101-
.filter(':visible')
102-
.first()
103-
.click();
104-
cy.contains(`Tenant invite: ${tenant2Name}`).first().click();
105-
106-
// Should be on the invites page now
10799
cy.location('pathname', { timeout: 5000 }).should(
108100
'eq',
109101
'/onboarding/invites',
@@ -112,14 +104,15 @@ describe('Tenant Invite: accept', () => {
112104
// Find the specific invite and accept it
113105
cy.contains(`invited to join the ${tenant2Name} tenant`).should('exist');
114106

115-
// Accept the invite
107+
// Step 4: Accept the invite - register intercept before clicking
116108
cy.intercept('POST', '/api/v1/users/invites/accept').as('acceptInvite');
117109
cy.contains(`invited to join the ${tenant2Name} tenant`)
118110
.parent()
119111
.contains('button', 'Accept')
120112
.should('exist')
121113
.click();
122114

115+
// Wait for the accept API call to complete
123116
cy.wait('@acceptInvite').its('response.statusCode').should('eq', 200);
124117

125118
// Wait for the accepted invite card to be removed from the DOM before
@@ -148,7 +141,7 @@ describe('Tenant Invite: accept', () => {
148141
};
149142
declineAll();
150143

151-
// Verify redirect to the tenant page
144+
// Step 5: Verify redirect to the tenant page (no infinite loop)
152145
cy.location('pathname', { timeout: 5000 }).should(
153146
'match',
154147
/\/tenants\/[^/]+/,

frontend/app/cypress/e2e/auth/06-tenant-switching.cy.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ describe('Tenants: switching', () => {
127127
.filter(':visible')
128128
.first()
129129
.should('contain.text', tenant2Name);
130-
cy.contains('button', 'Logout').filter(':visible').first().click();
130+
cy.get('button[aria-label="User Menu"]')
131+
.filter(':visible')
132+
.should('be.visible')
133+
.first()
134+
.click();
135+
// Menu item includes a keyboard shortcut, so match by substring.
136+
cy.contains('[role="menuitem"]', 'Log out').filter(':visible').click();
131137

132138
cy.login('member');
133139
cy.visit('/');
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import { seededUsers } from '../../support/seeded-users.generated';
2+
3+
describe('Create Tenant: redirect to invites', () => {
4+
it('should redirect to invites page when user has pending invites', () => {
5+
const ts = Date.now();
6+
const tenantName = `InviteRedirectTenant${ts}`;
7+
const tenantSlug = `invite-redirect-tenant-${ts}`;
8+
9+
// Step 1: Login as owner and create a tenant
10+
cy.visit('/auth/login');
11+
cy.get('input#email').type(seededUsers.owner.email);
12+
cy.get('input#password').type(seededUsers.owner.password);
13+
cy.get('form')
14+
.filter(':visible')
15+
.first()
16+
.within(() => {
17+
cy.contains('button', /^Sign In$/)
18+
.should('be.enabled')
19+
.click();
20+
});
21+
cy.location('pathname', { timeout: 30000 }).should(
22+
'match',
23+
/\/tenants\/.+/,
24+
);
25+
26+
// Create a new tenant for the invite
27+
cy.request({
28+
method: 'POST',
29+
url: '/api/v1/tenants',
30+
body: {
31+
name: tenantName,
32+
slug: tenantSlug,
33+
environment: 'development',
34+
},
35+
})
36+
.its('status')
37+
.should('eq', 200);
38+
39+
// Refresh to get the new tenant
40+
cy.visit('/');
41+
cy.location('pathname', { timeout: 30000 }).should(
42+
'match',
43+
/\/tenants\/.+/,
44+
);
45+
46+
// Switch to the new tenant
47+
cy.get('button[aria-label="Select a tenant"]')
48+
.filter(':visible')
49+
.first()
50+
.click({ force: true });
51+
cy.get('[data-cy="tenant-switcher-list"]').should('be.visible');
52+
cy.get(`[data-cy="tenant-switcher-item-${tenantSlug}"]`)
53+
.should('exist')
54+
.scrollIntoView()
55+
.click({ force: true });
56+
57+
// Get tenant ID from URL
58+
cy.location('pathname', { timeout: 30000 })
59+
.should('match', /\/tenants\/([^/]+)/)
60+
.then((pathname) => {
61+
const match = pathname.match(/\/tenants\/([^/]+)/);
62+
const tenantId = match![1];
63+
64+
// Step 2: Create an invite for the member user
65+
cy.request({
66+
method: 'POST',
67+
url: `/api/v1/tenants/${tenantId}/invites`,
68+
body: {
69+
email: seededUsers.member.email,
70+
role: 'MEMBER',
71+
},
72+
}).then((response) => {
73+
expect(response.status).to.eq(201);
74+
});
75+
});
76+
77+
// Step 3: Logout
78+
cy.get('button[aria-label="User Menu"]')
79+
.filter(':visible')
80+
.should('be.visible')
81+
.first()
82+
.click();
83+
cy.contains('[role="menuitem"]', 'Log out').filter(':visible').click();
84+
cy.location('pathname').should('include', '/auth/login');
85+
86+
// Step 4: Login as member (who has pending invite)
87+
cy.get('input#email').type(seededUsers.member.email);
88+
cy.get('input#password').type(seededUsers.member.password);
89+
cy.get('form')
90+
.filter(':visible')
91+
.first()
92+
.within(() => {
93+
cy.contains('button', /^Sign In$/)
94+
.should('be.enabled')
95+
.click();
96+
});
97+
98+
// Wait for the navigation after sign in to complete
99+
cy.location('pathname', { timeout: 30000 }).should('not.eq', '/auth/login');
100+
101+
// Step 5: Try to navigate to create-tenant page
102+
// The user should be redirected to invites page
103+
cy.visit('/onboarding/create-tenant', { failOnStatusCode: false });
104+
105+
// Step 6: Verify redirect to invites page
106+
cy.location('pathname', { timeout: 10000 }).should(
107+
'eq',
108+
'/onboarding/invites',
109+
);
110+
111+
// Verify the invite is displayed
112+
cy.contains(`invited to join the ${tenantName} tenant`).should(
113+
'be.visible',
114+
);
115+
116+
// Step 7: Accept the invite to clean up (prevent affecting other tests)
117+
cy.intercept('POST', '/api/v1/users/invites/accept').as('acceptInvite');
118+
cy.contains(`invited to join the ${tenantName} tenant`)
119+
.parent()
120+
.contains('button', 'Accept')
121+
.should('be.visible')
122+
.click();
123+
124+
cy.wait('@acceptInvite').its('response.statusCode').should('eq', 200);
125+
126+
// Verify redirect to tenant page
127+
cy.location('pathname', { timeout: 10000 }).should(
128+
'match',
129+
/\/tenants\/[^/]+/,
130+
);
131+
});
132+
});

frontend/app/cypress/e2e/auth/08-tenant-invite-decline.cy.ts

Lines changed: 18 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,12 @@ describe('Tenant Invite: decline', () => {
7575
});
7676

7777
// Step 3: Logout
78-
cy.contains('button', 'Logout').filter(':visible').first().click();
78+
cy.get('button[aria-label="User Menu"]')
79+
.filter(':visible')
80+
.should('be.visible')
81+
.first()
82+
.click();
83+
cy.contains('[role="menuitem"]', 'Log out').filter(':visible').click();
7984
cy.location('pathname').should('include', '/auth/login');
8085

8186
// Step 4: Login as member (who has pending invite)
@@ -90,23 +95,7 @@ describe('Tenant Invite: decline', () => {
9095
.click();
9196
});
9297

93-
// Wait for navigation after sign in
94-
cy.location('pathname', { timeout: 30000 }).should(
95-
'match',
96-
/\/tenants\/.+/,
97-
);
98-
99-
// Open the notification dropdown and click the tenant invite notification
100-
cy.get('[data-cy="notifications-button"]', { timeout: 10000 })
101-
.filter(':visible')
102-
.first()
103-
.click();
104-
cy.contains(`Tenant invite: ${tenantName}`)
105-
.filter(':visible')
106-
.first()
107-
.click();
108-
109-
// Should be on the invites page now
98+
// Should be redirected to invites page
11099
cy.location('pathname', { timeout: 5000 }).should(
111100
'eq',
112101
'/onboarding/invites',
@@ -117,25 +106,19 @@ describe('Tenant Invite: decline', () => {
117106
'be.visible',
118107
);
119108

120-
// Step 5: Decline all invites
121-
const declineAll = (remaining = 20) => {
122-
cy.get('body').then(($body) => {
123-
if (
124-
remaining > 0 &&
125-
$body.find('button:contains("Decline")').length > 0
126-
) {
127-
cy.intercept('POST', '/api/v1/users/invites/reject').as(
128-
'rejectInvite',
129-
);
130-
cy.contains('button', 'Decline').click({ force: true });
131-
cy.wait('@rejectInvite');
132-
declineAll(remaining - 1);
133-
}
134-
});
135-
};
136-
declineAll();
109+
// Step 5: Decline the invite - register intercept before clicking
110+
cy.intercept('POST', '/api/v1/users/invites/reject').as('rejectInvite');
111+
cy.contains(`invited to join the ${tenantName} tenant`)
112+
.parent()
113+
.contains('button', 'Decline')
114+
.should('be.visible')
115+
.click();
116+
117+
// Wait for the reject API call to complete
118+
cy.wait('@rejectInvite').its('response.statusCode').should('eq', 200);
137119

138120
// Step 6: Verify redirect away from invites page
121+
// User should be redirected to authenticated route (which may further redirect)
139122
cy.location('pathname', { timeout: 10000 }).should(
140123
'not.eq',
141124
'/onboarding/invites',

frontend/app/cypress/e2e/layout/v1-sidebar-resize-collapse.cy.ts

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
describe('v1 sidebar: resize + collapse', () => {
22
const DEFAULT_EXPANDED_WIDTH = 200;
33

4-
const visitAuthed = (
5-
viewport: { width: number; height: number },
6-
opts?: { requireVisibleSidebar?: boolean },
7-
) => {
4+
const visitAuthed = (viewport: { width: number; height: number }) => {
85
cy.viewport(viewport.width, viewport.height);
96
cy.login('owner');
107

@@ -17,16 +14,9 @@ describe('v1 sidebar: resize + collapse', () => {
1714
},
1815
});
1916

20-
if (opts?.requireVisibleSidebar !== false) {
21-
if (viewport.width < 768) {
22-
cy.get('button[aria-label="Toggle sidebar"]', { timeout: 30000 })
23-
.should('be.visible')
24-
.click();
25-
}
26-
27-
cy.get('[data-cy="v1-sidebar"]', { timeout: 30000 }).should('be.visible');
28-
}
29-
17+
cy.get('button[aria-label="User Menu"]', { timeout: 30000 }).should(
18+
'be.visible',
19+
);
3020
cy.location('pathname', { timeout: 30000 }).should(
3121
'match',
3222
/\/tenants\/.+/,
@@ -40,14 +30,17 @@ describe('v1 sidebar: resize + collapse', () => {
4030
};
4131

4232
const waitForShell = () => {
43-
cy.get('[data-cy="v1-sidebar"]', { timeout: 30000 }).should('be.visible');
33+
cy.get('button[aria-label="User Menu"]', { timeout: 30000 }).should(
34+
'be.visible',
35+
);
36+
cy.get('[data-cy="v1-sidebar"]').should('be.visible');
4437
};
4538

4639
it('navbar: sidebar toggle button is only visible on mobile', () => {
4740
visitAuthed({ width: 1280, height: 800 });
4841
cy.get('button[aria-label="Toggle sidebar"]').should('not.be.visible');
4942

50-
visitAuthed({ width: 375, height: 667 }, { requireVisibleSidebar: false });
43+
visitAuthed({ width: 375, height: 667 });
5144
cy.get('button[aria-label="Toggle sidebar"]').should('be.visible');
5245
});
5346

@@ -196,19 +189,21 @@ describe('v1 sidebar: resize + collapse', () => {
196189
expectSidebarWidthStyle(56);
197190
});
198191

199-
it('collapsed: settings items are always visible without a flyout', () => {
192+
it('collapsed: settings flyout renders and has a visible panel background', () => {
200193
visitAuthed({ width: 1280, height: 800 });
201194

202195
// Collapse.
203196
cy.get('[data-cy="v1-sidebar-resize-handle"]').click({ force: true });
204-
expectSidebarWidthStyle(56);
205197

206-
// Settings items exist directly — no flyout needed.
207-
cy.get('button[aria-label="API Tokens"]')
208-
.scrollIntoView()
209-
.should('be.visible');
210-
cy.get('button[aria-label="Members"]')
211-
.scrollIntoView()
212-
.should('be.visible');
198+
// Open settings flyout.
199+
cy.get('button[aria-label="General"]').click({ force: true });
200+
cy.get('[role="menu"]').filter(':visible').first().as('settingsMenu');
201+
cy.get('@settingsMenu').contains('Overview').should('be.visible');
202+
203+
// Content should have the bg-secondary class (explicit panel surface).
204+
cy.get('@settingsMenu')
205+
.invoke('attr', 'class')
206+
// UI uses popover surfaces; accept either explicit secondary surface or popover surface.
207+
.should('match', /\bbg-(secondary|popover)\b/);
213208
});
214209
});

0 commit comments

Comments
 (0)