Skip to content

Commit 6285e2d

Browse files
committed
Merge branch 'dev' into NO-ISSUE-fix-sidebar-performance-real-time-events
2 parents 3a121ef + 8d5a562 commit 6285e2d

82 files changed

Lines changed: 11424 additions & 744 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Template Engine E2E Tests (mocked)
2+
3+
# Runs the mocked Template Engine end-to-end specs on every PR targeting `dev`
4+
# and `main`. The specs mock every backend call they need (including auth via
5+
# `cy.loginMock`), so this only needs the app running locally — no credentials
6+
# or network access to the real backend.
7+
on:
8+
pull_request:
9+
types: [opened, synchronize, reopened, ready_for_review]
10+
branches:
11+
- dev
12+
- main
13+
14+
env:
15+
HUSKY: 0
16+
17+
permissions:
18+
contents: read
19+
20+
jobs:
21+
template-engine-e2e:
22+
name: Run Template Engine mocked E2E
23+
runs-on: ubuntu-latest
24+
steps:
25+
- name: Checkout code
26+
uses: actions/checkout@v4
27+
28+
- name: Setup Node.js
29+
uses: actions/setup-node@v4
30+
with:
31+
node-version: 22.18.0
32+
cache: 'yarn'
33+
34+
- name: Run Cypress (template-engine mocked specs)
35+
uses: cypress-io/github-action@v6
36+
with:
37+
browser: chrome
38+
start: yarn dev --logLevel=warn
39+
wait-on: 'http://localhost:5173/'
40+
wait-on-timeout: 120
41+
spec: cypress/e2mock/template-engine/**/*.cy.js
42+
43+
- name: Upload Cypress Screenshots
44+
if: ${{ failure() }}
45+
uses: actions/upload-artifact@v4
46+
with:
47+
name: cypress_screenshots
48+
path: cypress/screenshots
49+
retention-days: 4
50+
51+
- name: Upload Cypress Videos
52+
if: ${{ failure() }}
53+
uses: actions/upload-artifact@v4
54+
with:
55+
name: cypress_videos
56+
path: cypress/videos
57+
retention-days: 1
Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
/* eslint-disable cypress/unsafe-to-chain-command */
2+
import selectors from '../../support/selectors'
3+
4+
// Fixtures
5+
import accountInfo from '../../fixtures/template-engine/account-info.json'
6+
import solutionsList from '../../fixtures/template-engine/solutions-list.json'
7+
import solution from '../../fixtures/template-engine/solution.json'
8+
import template from '../../fixtures/template-engine/template-no-settings.json'
9+
import vcsIntegrations from '../../fixtures/template-engine/vcs-integrations.json'
10+
import logsFailed from '../../fixtures/template-engine/logs-failed.json'
11+
12+
const templateEngine = selectors.templateEngine
13+
14+
const VENDOR_SLUG = 'azion'
15+
const SOLUTION_SLUG = 'my-template-e2e'
16+
const TEMPLATE_ID = 'tmpl-e2e-123'
17+
const EXECUTION_ID = 'exec-fail-uuid'
18+
const PROJECT_NAME = 'failure-project-e2e'
19+
const POLL_INTERVAL_MS = 7000
20+
21+
const fillRepositoryStep = () => {
22+
cy.get(templateEngine.navbarCreateButton).click()
23+
cy.get(templateEngine.modalContainer).should('be.visible')
24+
cy.contains(templateEngine.modalMenuItem, 'Templates').click()
25+
cy.get(templateEngine.modalTemplatesItem).first().click()
26+
27+
cy.wait('@loadSolution')
28+
cy.wait('@getTemplate')
29+
cy.wait('@getVcsIntegrations')
30+
31+
cy.get(templateEngine.gitScopeDropdown).should('be.visible').click()
32+
cy.contains(templateEngine.dropdownItem, 'azion-e2e-org').click()
33+
cy.get(templateEngine.gitScopeDropdown).should('contain', 'azion-e2e-org')
34+
35+
cy.get(templateEngine.projectNameInput).clear()
36+
cy.get(templateEngine.projectNameInput).type(PROJECT_NAME)
37+
}
38+
39+
describe('Template Engine - Deploy failure flow (mocked)', { tags: ['@dev3'] }, () => {
40+
beforeEach(() => {
41+
cy.intercept(
42+
{ method: 'GET', url: '**/api/account/info' },
43+
{ statusCode: 200, body: accountInfo }
44+
).as('getAccountInfo')
45+
46+
cy.intercept(
47+
{ method: 'GET', pathname: '/api/marketplace/solution/' },
48+
{ statusCode: 200, body: solutionsList }
49+
).as('listSolutions')
50+
51+
cy.intercept(
52+
{ method: 'GET', pathname: `/api/marketplace/solution/${VENDOR_SLUG}/${SOLUTION_SLUG}` },
53+
{ statusCode: 200, body: solution }
54+
).as('loadSolution')
55+
56+
cy.intercept(
57+
{ method: 'GET', pathname: `/api/template-engine/templates/${TEMPLATE_ID}` },
58+
{ statusCode: 200, body: template }
59+
).as('getTemplate')
60+
61+
cy.intercept(
62+
{ method: 'GET', url: '**/v4/vcs/integrations*' },
63+
{ statusCode: 200, body: vcsIntegrations }
64+
).as('getVcsIntegrations')
65+
66+
cy.loginMock()
67+
cy.wait('@getAccountInfo')
68+
})
69+
70+
it('shows the error in the logs and does not reach the success state', () => {
71+
let logsCall = 0
72+
73+
cy.intercept(
74+
{ method: 'POST', pathname: `/api/template-engine/templates/${TEMPLATE_ID}/instantiate` },
75+
{ statusCode: 201, body: { uuid: EXECUTION_ID } }
76+
).as('instantiate')
77+
78+
// First poll: running. Second poll: failed (with an error log).
79+
cy.intercept(
80+
{ method: 'GET', pathname: `/api/script-runner/executions/${EXECUTION_ID}/logs` },
81+
(req) => {
82+
logsCall += 1
83+
if (logsCall >= 2) {
84+
req.reply(logsFailed)
85+
} else {
86+
req.reply({
87+
statusCode: 200,
88+
body: {
89+
status: 'running',
90+
logs: [{ content: 'Starting deployment', timestamp: '2026-06-10T12:00:00.000Z' }]
91+
}
92+
})
93+
}
94+
}
95+
).as('getLogs')
96+
97+
// Results requested on finish - return an error envelope so the app never
98+
// transitions to the success step.
99+
cy.intercept(
100+
{ method: 'GET', pathname: `/api/script-runner/executions/${EXECUTION_ID}/results` },
101+
{ statusCode: 200, body: { result: { errors: { message: 'Failed to provision resources' } } } }
102+
).as('getResults')
103+
104+
fillRepositoryStep()
105+
106+
// Freeze timers to drive the logs polling
107+
cy.clock(null, ['setTimeout', 'setInterval'])
108+
109+
cy.get(templateEngine.deployButton).should('not.be.disabled').click()
110+
cy.wait('@instantiate').its('response.statusCode').should('eq', 201)
111+
112+
cy.get(templateEngine.deployStatusCard).should('be.visible')
113+
114+
// First poll => running
115+
cy.tick(POLL_INTERVAL_MS)
116+
cy.wait('@getLogs')
117+
cy.get(templateEngine.deployStatusCard).should('contain', 'Starting deployment')
118+
119+
// Second poll => failed
120+
cy.tick(POLL_INTERVAL_MS)
121+
cy.wait('@getLogs')
122+
cy.wait('@getResults')
123+
124+
// Error surfaced in the logs
125+
cy.get(templateEngine.deployStatusCard).should('contain', 'Failed to provision resources')
126+
127+
// No success transition: inputs card still present, success card not shown
128+
cy.get(templateEngine.inputsCard).should('be.visible')
129+
cy.get(templateEngine.deploySuccessCard).should('not.be.visible')
130+
cy.get(templateEngine.deployStatusCard).should('be.visible')
131+
})
132+
133+
it('handles an instantiate failure without starting the polling', () => {
134+
cy.intercept(
135+
{ method: 'POST', pathname: `/api/template-engine/templates/${TEMPLATE_ID}/instantiate` },
136+
{ statusCode: 500, body: { message: 'Internal Server Error' } }
137+
).as('instantiate')
138+
139+
fillRepositoryStep()
140+
141+
cy.get(templateEngine.deployButton).should('not.be.disabled').click()
142+
cy.wait('@instantiate').its('response.statusCode').should('eq', 500)
143+
144+
// No execution id => logs card never renders, no success card
145+
cy.get(templateEngine.deployStatusCard).should('not.exist')
146+
cy.get(templateEngine.deploySuccessCard).should('not.be.visible')
147+
148+
// Inputs remain available and the deploy can be retried
149+
cy.get(templateEngine.inputsCard).should('be.visible')
150+
cy.get(templateEngine.deployButton).should('not.be.disabled')
151+
})
152+
153+
it('allows retrying the deploy after an instantiate failure', () => {
154+
let instantiateCall = 0
155+
156+
cy.intercept(
157+
{ method: 'POST', pathname: `/api/template-engine/templates/${TEMPLATE_ID}/instantiate` },
158+
(req) => {
159+
instantiateCall += 1
160+
if (instantiateCall === 1) {
161+
req.reply({ statusCode: 500, body: { message: 'Internal Server Error' } })
162+
} else {
163+
req.reply({ statusCode: 201, body: { uuid: EXECUTION_ID } })
164+
}
165+
}
166+
).as('instantiate')
167+
168+
// Keep polling benign on the successful retry (timers stay frozen, so it
169+
// never actually fires, but this prevents any real request from leaking).
170+
cy.intercept(
171+
{ method: 'GET', pathname: `/api/script-runner/executions/${EXECUTION_ID}/logs` },
172+
{ statusCode: 200, body: { status: 'running', logs: [] } }
173+
).as('getLogs')
174+
175+
fillRepositoryStep()
176+
177+
// First attempt fails
178+
cy.get(templateEngine.deployButton).should('not.be.disabled').click()
179+
cy.wait('@instantiate').its('response.statusCode').should('eq', 500)
180+
cy.get(templateEngine.deployStatusCard).should('not.exist')
181+
182+
// Freeze timers before the retry so the polling doesn't run real-time
183+
cy.clock(null, ['setTimeout', 'setInterval'])
184+
185+
// Retry: second click triggers a new instantiate that succeeds
186+
cy.get(templateEngine.deployButton).should('not.be.disabled').click()
187+
cy.wait('@instantiate').its('response.statusCode').should('eq', 201)
188+
189+
// Flow advances to the deployment step (logs card shown)
190+
cy.get(templateEngine.deployStatusCard).should('be.visible')
191+
})
192+
})

0 commit comments

Comments
 (0)