Skip to content

Commit fef5ec2

Browse files
authored
Merge pull request #1048
* refactor(34332): add a paginated list of bridges * refactor(34332): add bridge action menu * fix(34332): fix icons * refactor(34332): add support for edit and delete * fix(34332): fix translations * fix(34332): fix translations * fix(34332): fix the no data message * refactor(34332): add JSON/UI schemas for the bridge editor * refactor(34332): add the bridge editor * refactor(34332): add conditional schemas for the toggled features * refactor(34332): fix defaults and required * refactor(34332): update title, description and widget for all properties * fix(34332): update props * fix(34332): add identifier to the recognised formats * refactor(34332): add a Toggle widget * fix(34332): fix wrap in tablist * refactor(34332): add support for create/edit bridge and persistence * fix(34332): fix widget definition * fix(34332): fix translations * refactor(34332): add custom validation (unique id) * fix(34332): fix layout * refactor(34332): add request manager for the bridge * refactor(34332): replace the old editor * refactor(34332): replace the old list of bridges * fix(34332): fix CTAs and header * refactor(34332): add initial form data, request handling and errors * refactor(34332): change the default state of new bridges * refactor(34332): fix translations * test(34332): fix mocks * test(34332): fix test * test(34332): fix test * fix(34332): fix lazy loading * fix(34332): fix no-break space * test(34332): add tests * test(34332): add tests * test(34332): add tests * test(34332): fix tests * fix(34332): redesign error message and rename file * fix(34332): fix error message * test(34332): add test * fix(34332): fix translations * fix(34332): fix initialisation of editor * test(34332): fix tests * refactor(34332): add collapsable to the list of subscriptions * fix(34332): fix tag/topic/fitler widgets in RJSF forms * fix(34332): add tag/topic/fitler widgets to the schema * test(34332): add tests * test(34332): fix tests * test(34332): add tests * Update hivemq-edge-frontend/src/modules/Bridges/components/BridgeEdit… * Update hivemq-edge-frontend/src/modules/Bridges/utils/validation-util… * Update hivemq-edge-frontend/src/modules/Bridges/utils/validation-util… * fix(34332): fix copilot stupid review * fix(34332): linting * fix(34332): fix id suffix * test(34332): add tests * fix(34332): fix definition of maxQoS * test(34332): fix tests * test(34332): refactor page object * test(34332): add page object for Bridges * test(34332): add E2E tests for Bridges * fix(34332): linting * fix(34332): fix translations * test(34332): fix exclusion * test(34332): fix translations * test(34332): fix translations * test(34332): fix hook dependencies * test(34332): linting
1 parent c1607d6 commit fef5ec2

39 files changed

+2278
-74
lines changed
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
import { drop, factory, primaryKey } from '@mswjs/data'
2+
3+
import type { Bridge } from '@/api/__generated__'
4+
import { cy_interceptCoreE2E } from 'cypress/utils/intercept.utils.ts'
5+
import { bridgePage, loginPage, rjsf } from 'cypress/pages'
6+
7+
describe('Bridges', () => {
8+
// Creating a mock storage for the Bridges
9+
const mswDB = factory({
10+
bridge: {
11+
id: primaryKey(String),
12+
json: String,
13+
},
14+
})
15+
16+
beforeEach(() => {
17+
drop(mswDB)
18+
19+
cy_interceptCoreE2E()
20+
21+
// TODO[NVL] Add support for the Event Log
22+
cy.intercept('GET', '/api/v1/management/bridges', (req) => {
23+
const allBridgeData = mswDB.bridge.getAll()
24+
const allBridges = allBridgeData.map<Bridge>((data) => ({ ...JSON.parse(data.json) }))
25+
req.reply(200, { items: allBridges })
26+
})
27+
28+
cy.intercept<Bridge>('POST', '/api/v1/management/bridges', (req) => {
29+
const bridge = req.body
30+
const newBridgeData = mswDB.bridge.create({
31+
id: bridge.id,
32+
json: JSON.stringify(bridge),
33+
})
34+
req.reply(200, newBridgeData)
35+
})
36+
37+
cy.intercept<Bridge>('PUT', '/api/v1/management/bridges/**', (req) => {
38+
const bridge = req.body
39+
40+
mswDB.bridge.update({
41+
where: {
42+
id: {
43+
equals: bridge.id,
44+
},
45+
},
46+
47+
data: { json: JSON.stringify(bridge) },
48+
})
49+
50+
req.reply(200, '')
51+
})
52+
53+
cy.intercept<Bridge>('DELETE', '/api/v1/management/bridges/**', (req) => {
54+
const urlParts = req.url.split('/')
55+
const bridgeId = urlParts[urlParts.length - 1]
56+
57+
mswDB.bridge.delete({
58+
where: {
59+
id: {
60+
equals: bridgeId,
61+
},
62+
},
63+
})
64+
req.reply(200, '')
65+
}).as('deleteBridge')
66+
67+
loginPage.visit('/app/mqtt-bridges')
68+
loginPage.loginButton.click()
69+
bridgePage.navLink.click()
70+
})
71+
72+
context('Bridge configuration', () => {
73+
// TODO[NVL] Add support for the toasts
74+
75+
it('should render the landing page', () => {
76+
cy.location().should((loc) => {
77+
expect(loc.pathname).to.eq('/app/mqtt-bridges')
78+
})
79+
80+
bridgePage.pageHeader.should('have.text', 'MQTT bridges')
81+
bridgePage.pageHeaderSubTitle.should(
82+
'have.text',
83+
'MQTT bridges let you connect multiple MQTT brokers to enable seamless data sharing between different networks or systems.'
84+
)
85+
bridgePage.addNewBridge.should('have.text', 'Add bridge connection')
86+
87+
bridgePage.table.container.should('be.visible')
88+
bridgePage.table.status
89+
.should('have.attr', 'data-status', 'info')
90+
.should('have.text', 'No bridges currently created')
91+
})
92+
93+
it('should create a new bridge', () => {
94+
cy.location().should((loc) => {
95+
expect(loc.pathname).to.eq('/app/mqtt-bridges')
96+
})
97+
98+
bridgePage.table.status
99+
.should('have.attr', 'data-status', 'info')
100+
.should('have.text', 'No bridges currently created')
101+
102+
bridgePage.addNewBridge.click()
103+
104+
cy.location().should((loc) => {
105+
expect(loc.pathname).to.eq('/app/mqtt-bridges/new')
106+
})
107+
108+
bridgePage.config.panel.should('be.visible')
109+
bridgePage.config.title.should('have.text', 'Create a new bridge configuration')
110+
bridgePage.config.submitButton.should('have.text', 'Create the bridge')
111+
112+
bridgePage.config.errorSummary.should('have.length', 3)
113+
bridgePage.config.errorSummaryFocus(0).should('exist')
114+
115+
rjsf.field('id').input.should('not.have.value')
116+
rjsf.field('id').input.type('my-bridge')
117+
rjsf.field('host').input.type('my-host')
118+
rjsf.field('clientId').input.type('my-client-id')
119+
bridgePage.config.errorSummary.should('have.length', 0)
120+
121+
bridgePage.config.submitButton.click()
122+
123+
cy.location().should((loc) => {
124+
expect(loc.pathname).to.eq('/app/mqtt-bridges')
125+
})
126+
127+
bridgePage.table.rows.should('have.length', 1)
128+
bridgePage.table.cell(0, 'id').should('have.text', 'my-bridge')
129+
130+
bridgePage.table.action(0, 'edit').click()
131+
132+
rjsf.field('id').input.should('be.disabled')
133+
rjsf.field('port').input.clear().type('2999')
134+
135+
bridgePage.config.submitButton.click()
136+
137+
//TODO[NVL] Better create a subscription
138+
})
139+
140+
it('should create a new bridge and delete it', () => {
141+
cy.location().should((loc) => {
142+
expect(loc.pathname).to.eq('/app/mqtt-bridges')
143+
})
144+
145+
bridgePage.table.status
146+
.should('have.attr', 'data-status', 'info')
147+
.should('have.text', 'No bridges currently created')
148+
149+
bridgePage.addNewBridge.click()
150+
151+
bridgePage.config.panel.should('be.visible')
152+
bridgePage.config.submitButton.should('have.text', 'Create the bridge')
153+
154+
rjsf.field('id').input.type('my-bridge')
155+
rjsf.field('host').input.type('my-host')
156+
rjsf.field('clientId').input.type('my-client-id')
157+
158+
bridgePage.config.submitButton.click()
159+
160+
bridgePage.table.action(0, 'delete').click()
161+
bridgePage.modal.dialog.should('be.visible')
162+
bridgePage.modal.title.should('have.text', 'Delete Bridge')
163+
bridgePage.modal.prompt.should('have.text', "Are you sure? You can't undo this action afterward.")
164+
bridgePage.modal.confirm.should('have.text', 'Delete')
165+
bridgePage.modal.confirm.click()
166+
167+
cy.wait('@deleteBridge')
168+
bridgePage.table.status
169+
.should('have.attr', 'data-status', 'info')
170+
.should('have.text', 'No bridges currently created')
171+
})
172+
})
173+
174+
context('Bridge in Workspace', () => {
175+
it('should create a bridge also in the Workspace', () => {
176+
bridgePage.table.status.should('have.text', 'No bridges currently created')
177+
178+
bridgePage.addNewBridge.click()
179+
180+
bridgePage.config.panel.should('be.visible')
181+
bridgePage.config.title.should('have.text', 'Create a new bridge configuration')
182+
bridgePage.config.submitButton.should('have.text', 'Create the bridge')
183+
184+
rjsf.field('id').input.type('my-bridge')
185+
rjsf.field('host').input.type('my-host')
186+
rjsf.field('clientId').input.type('my-client-id')
187+
188+
bridgePage.config.submitButton.click()
189+
190+
//TODO[NVL] Better create a subscription
191+
})
192+
})
193+
194+
context('Bridge in Event Log', () => {})
195+
196+
it('should be accessible', () => {
197+
cy.injectAxe()
198+
bridgePage.table.status.should('have.text', 'No bridges currently created')
199+
200+
bridgePage.addNewBridge.click()
201+
202+
bridgePage.config.panel.should('be.visible')
203+
bridgePage.config.title.should('have.text', 'Create a new bridge configuration')
204+
bridgePage.config.submitButton.should('have.text', 'Create the bridge')
205+
206+
rjsf.field('id').input.type('my-bridge')
207+
rjsf.field('host').input.type('my-host')
208+
rjsf.field('clientId').input.type('my-client-id')
209+
210+
bridgePage.config.submitButton.click()
211+
212+
bridgePage.toast.close()
213+
214+
bridgePage.table.action(0, 'edit').click()
215+
216+
bridgePage.config.formTab(4).click() // Click on the "Subscriptions" tab
217+
218+
cy.checkAccessibility()
219+
})
220+
})
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import { Page } from '../Page.ts'
2+
3+
export class BridgePage extends Page {
4+
get navLink() {
5+
return cy.get('nav [role="list"]').eq(0).find('li').eq(2)
6+
}
7+
8+
get addNewBridge() {
9+
return cy.getByTestId('page-container-cta').find('button')
10+
}
11+
12+
table = {
13+
get container() {
14+
return cy.get('table[aria-label="List of bridges"]')
15+
},
16+
17+
get status() {
18+
return cy.get('table[aria-label="List of bridges"] tbody tr td[colspan="6"] div[role="alert"]')
19+
},
20+
21+
get rows() {
22+
return cy.get('table[aria-label="List of bridges"] tbody tr')
23+
},
24+
25+
row(index: number) {
26+
return this.rows.eq(index)
27+
},
28+
29+
cell(
30+
row: number,
31+
column: number | 'id' | 'localSubscriptions' | 'remoteSubscriptions' | 'status' | 'lastStarted' | 'actions'
32+
) {
33+
const map = ['id', 'localSubscriptions', 'remoteSubscriptions', 'status', 'lastStarted', 'actions']
34+
if (typeof column === 'string') return this.rows.eq(row).get('td').eq(map.indexOf(column))
35+
else return this.rows.eq(row).get('td').eq(column)
36+
},
37+
38+
action(row: number, action: 'stop' | 'start' | 'restart' | 'edit' | 'delete') {
39+
const map = {
40+
stop: 'device-action-stop',
41+
start: 'device-action-start',
42+
restart: 'device-action-restart',
43+
edit: 'bridge-action-edit',
44+
delete: 'bridge-action-delete',
45+
}
46+
this.cell(row, 5).within(() => {
47+
cy.get('button').click()
48+
})
49+
return cy.get(`[role="menu"] button[data-testid=${map[action]}]`)
50+
},
51+
}
52+
53+
modal = {
54+
get dialog() {
55+
return cy.get('[role="alertdialog"]')
56+
},
57+
58+
get title() {
59+
return this.dialog.find('header')
60+
},
61+
62+
get prompt() {
63+
return this.dialog.find('p[data-testid="confirmation-message"]')
64+
},
65+
66+
get confirm() {
67+
return this.dialog.find('button[data-testid="confirmation-submit"]')
68+
},
69+
}
70+
71+
config = {
72+
get panel() {
73+
return cy.get('[role="dialog"][aria-label="Edit bridge configuration"]')
74+
},
75+
76+
get title() {
77+
return cy.get('[role="dialog"] header')
78+
},
79+
80+
get submitButton() {
81+
return cy.get('[role="dialog"] footer button[type="submit"]')
82+
},
83+
84+
get errorSummary() {
85+
return cy.get('[role="dialog"] form div[role="alert"] ul li')
86+
},
87+
88+
errorSummaryFocus(index: number) {
89+
return cy
90+
.get('[role="dialog"] form div[role="alert"] ul li')
91+
.eq(index)
92+
.within(() => {
93+
return cy.getByAriaLabel('Jump to error')
94+
})
95+
},
96+
97+
get formTabs() {
98+
return cy.get('[role="dialog"] form [role="tablist"]')
99+
},
100+
101+
formTab(index: number) {
102+
return this.formTabs.within(() => {
103+
return cy.get('button').eq(index)
104+
})
105+
},
106+
107+
get formTabPanel() {
108+
return cy.get('[role="dialog"] form [role="tabpanel"]')
109+
},
110+
111+
// This is experimental and should only be used within the form
112+
get formField() {
113+
return cy.get('div > div.field > [role="group"]')
114+
},
115+
}
116+
}
117+
118+
export const bridgePage = new BridgePage()

hivemq-edge-frontend/cypress/pages/Page.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,20 @@ export abstract class Page {
1212
},
1313
})
1414
}
15+
16+
get pageHeader() {
17+
return cy.get('main header > h1')
18+
}
19+
20+
get pageHeaderSubTitle() {
21+
return cy.get('main header > p')
22+
}
23+
24+
toast = {
25+
close() {
26+
cy.get('[role="status"]').within(() => {
27+
cy.getByAriaLabel('Close').click()
28+
})
29+
},
30+
}
1531
}

hivemq-edge-frontend/cypress/pages/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ import { loginPage } from './Login/LoginPage.ts'
22
import { adapterPage } from './Protocols/AdapterPage.ts'
33
import { rjsf } from './RJSF/RJSFomField.ts'
44
import { workspacePage } from './Workspace/WorkspacePage'
5+
import { bridgePage } from './Bridges/BridgePage.ts'
56

6-
export { loginPage, workspacePage, adapterPage, rjsf }
7+
export { loginPage, workspacePage, adapterPage, rjsf, bridgePage }

hivemq-edge-frontend/src/api/hooks/useGetBridges/__handlers__/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const mockBridge: Bridge = {
2424
clientId: 'my-client-id',
2525
status: {
2626
connection: Status.connection.CONNECTED,
27+
startedAt: '2023-08-21T11:51:24.234+01',
2728
},
2829
localSubscriptions: [
2930
{

hivemq-edge-frontend/src/api/hooks/useGetBridges/useGetBridge.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ describe('useGetBridge', () => {
2828
id: 'bridge-id-01',
2929
status: {
3030
connection: 'CONNECTED',
31+
startedAt: '2023-08-21T11:51:24.234+01',
3132
},
3233
})
3334
)

0 commit comments

Comments
 (0)