Install
$ agentstack add skill-cumulocity-iot-cumulocity-skills-community-write-e2e-cumulocity-cypress-test ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Write and verify e2e Cypress tests for Cumulocity
You are writing deterministic Cypress test code (no cy.prompt()), validating it yourself in a loop until it passes. A test is not done until you have run it headlessly and seen it green.
Workflow at a glance:
- Preflight — verify
.env, dev server, tenant/base-URL match, device ID. - Discover — walk the flow in a live browser (selectors, requests, response shapes), then confirm the backend contract in the Angular service source.
- Write — standard suite skeleton, intercept every relevant request before visiting, assert via stable selectors.
- Verify — run headless in a loop, diagnose from the request trace, fix, repeat; full suite once when green.
- Finish — delete run logs, report pass/fail counts, fold new lessons back into this skill.
This skill is project-agnostic. Placeholders in angle brackets (`, `, …) must be replaced with names discovered in the repo you are working in — never invent them.
Phase 0 — Preflight: verify the environment before writing anything
Run these checks first. Every minute spent here saves a failed-run iteration later.
- Learn the project's env contract. Read
cypress.config.ts(andcypress/support/e2e.ts) to see which.env/environment keys the project reads and how they map into Cypress. Typical keys:C8Y_TENANT,C8Y_BASEURL,C8Y_USERNAME,C8Y_PASSWORD,C8Y_SHELL_TARGET, plus project-specific ones for the dev-server URL and a test device ID. Key names in.envoften differ from what specs read viaCypress.env(...)— the config file is the authority on that mapping.
Verify .env exists and contains the required keys. Check key names only (cut -d= -f1 .env) — never print values. .env values are not exported to your shell; to use one in a command, extract it into a variable first: ``bash CYURL=$(grep '^=' .env | cut -d= -f2- | tr -d '\r') ``
- Dev server is up:
curl -s -o /dev/null -w "%{http_code}" "$CYURL/apps//index.html"must return 200. If not, ask the user to start it (usuallynpm run start); do not start it yourself unless asked. - Tenant ID matches the base URL. This is a frequent silent breaker:
.envfiles get copied between environments. Verify with the public endpoint:
``bash curl -s "$C8Y_BASEURL/tenant/loginOptions" ` The self links reveal the real tenant ID (e.g. https://t12345.…). If it differs from C8Y_TENANT, fix .env. Symptom of a mismatch: login fails with HTTP 400 "A tenant is required in context!"`.
- Device ID exists (only if the test needs a device context):
``bash curl -s -o /dev/null -w "%{http_code}" -u "$C8Y_USERNAME:$C8Y_PASSWORD" "$C8Y_BASEURL/inventory/managedObjects/" ` On 404, find a real one: GET /inventory/managedObjects?fragmentType=c8y_IsDevice&pageSize=3 and update .env`.
- Dependencies are pre-installed (
cumulocity-cypress, and ideallycypress-terminal-reportfor request traces). Do not install anything new without asking.
Phase 1 — Discover the feature: browser first, then source
1a. Walk the flow live in the browser
If a logged-in browser session against the dev server is available (Chrome DevTools MCP or Playwright MCP), always start there — before reading node_modules or querying any SDK knowledge source. Navigate the actual user flow you are about to test and record, in one pass:
- The rendered DOM: enumerate
data-cyattributes, component tags,title/aria-labelhooks on every element the test will touch (a smallevaluate_scriptthat maps buttons/cells/tabs to their attributes does this in seconds). This yields the real selectors — including the built-in hooks of@c8y/ngx-componentswidgets — with zero guessing. - The network traffic: which requests fire on route load vs. on user action, their order, methods, and query params. This confirms the contract you derive from source in 1b and reveals side calls (permission lookups,
currentTenant, option fetches) the source reading might miss. - The real response shapes: copy a live response as the template for mock bodies instead of reconstructing them from interfaces.
Do not try to discover widget internals by grepping minified @c8y/ngx-components bundles — it wastes time and fails. Fallback order when no live browser is available: an existing passing spec in the repo → the c8y-web-sdk-knowledge MCP (if configured) for component APIs and hooks → run the test once and read the DOM from the failure screenshot/trace.
1b. Confirm the backend contract from the source code
Never guess intercept URLs. The single most common e2e regression is an intercept pointing at a renamed or wrong endpoint — the test then times out with "No request ever occurred" while the real request 404s unmocked. The browser shows what fires today; the source is authoritative for what the intercept must match (and for branches the live environment cannot reach, e.g. a management-tenant-only code path).
Before writing a test for a component:
- Read the Angular service(s) the component injects (
*.service.ts). Collect everyBASE_URL/endpoint constant and everyfetchClient.fetch(...),measurementService.*,eventService.*,tenantService.*call. - For each user flow the test covers, list the requests it triggers, grouped by trigger:
- on init (
ngOnInit): usually GETs that populate forms/grids — these fire during page load, so their intercepts must be registered beforecy.visit/the shell visit command. - on action (save/delete/confirm): POST/PUT/DELETE — intercept and assert the request body.
- side calls the component makes that could interfere (validity checks, audit events, permission lookups).
- Prefer importing URL constants from the app code into the spec when they are exported (compile-time protection against renames). If they are not exported, consider exporting them as part of your change.
Phase 2 — Write the test
File location and naming
- New AI-written specs go to
cypress/e2e/(orcypress/e2e/ai-generated/if that folder convention exists in the repo). TypeScript,.cy.ts. - One
describeper feature area;itnames state observable behavior ("Should show error alert when updating the configuration fails").
Mandatory suite skeleton
///
describe('', () => {
// only if the test needs a device context (check cypress.config.ts for the exact env key):
const deviceId = Cypress.env('C8Y_DEVICE_ID');
before(() => {
// 'administration' | 'devicemanagement' | 'cockpit' — pick the shell hosting the plugin
Cypress.env('C8Y_SHELL_TARGET', 'administration');
Cypress.session.clearAllSavedSessions();
});
beforeEach(() => {
// cumulocity-cypress: OAuth login via cy.request + session caching. NEVER hand-roll login.
cy.getAuth().login().disableGainsight();
});
it('...', () => {
// 1. intercepts 2. visit 3. interact 4. wait on aliases 5. assert UI
});
});
Navigation
Check cypress/support/commands.ts for an existing project-local shell-visit command before writing navigation code — plugin repos usually define one. The common pattern is a command like visitShellAndWaitForSelector(url, language, selector) that prefixes /apps//index.html#/, appends the plugin remotes query string (often from a C8Y_SHELL_EXTENSION env var), calls cy.setLanguage(language), visits, and waits for an anchor selector to be visible. If the project has no such command, add one following that pattern rather than inlining cy.visit calls in every test.
// Shell root (administration/cockpit):
cy.visitShellAndWaitForSelector('', 'en', '#navigator');
// Device context:
cy.visitShellAndWaitForSelector(`device/${deviceId}`, 'en', 'c8y-tabs-outlet');
Have one test navigate by clicking (navigator entry, tab), so hookNavigator/hookTab registration is covered:
cy.get('#navigator button[data-cy=""]').click();
cy.get('c8y-tabs-outlet span[title=""]').click();
The remaining tests may deep-link to the route for speed — pass the plugin component's selector as the anchor:
cy.visitShellAndWaitForSelector('', 'en', '');
Intercept and wait — the rules
- Register every intercept before the visit. Init-time requests fire during page load; a late intercept never matches.
- Anything not intercepted hits the real backend through the dev proxy. All mutating requests (POST/PUT/DELETE) the flow triggers must be intercepted — a test must never write to the real tenant. Read-only GETs may pass through, but intercept the ones your assertions depend on.
- Exact paths from Phase 1b, glob-prefixed:
'**/service//'. Add a trailing*only when the app appends query params ('**/measurement/measurements*'). Globs are anchored:**/x/configdoes not match/x/config/details— mock each endpoint separately. - One alias per route+method, named after intent:
getConfiguration,updateConfigurationFail. cy.wait('@alias')before asserting dependent UI, and assert outgoing payloads on it:
``typescript cy.wait('@updateConfiguration') .its('request.body') .should('deep.equal', { name: testValue }); ``
- Test failure paths too: same intercept with
statusCode: 500and assert the error UI (e.g.c8y-alert-outlet div[data-cy="c8y-alert--message"].alert-danger). - Mock response bodies must match the TypeScript interfaces the service parses — copy the shape from a live response (Phase 1a) or the model files, not from memory.
- Steer app branches by mutating real responses with
req.continue((res) => …)instead of replacing the whole body — e.g. fake the management tenant or pin the tenant domain while keeping everything else real:
``typescript cy.intercept('GET', '**/tenant/currentTenant*', (req) => { req.continue((res) => { res.body.name = 'management'; res.body.customProperties = { ...(res.body.customProperties || {}), gainsightEnabled: false, // see rule 9 }; res.send(); }); }); ``
disableGainsight()shadowing: cumulocity-cypress disables Gainsight via its own intercept on/tenant/currentTenant*. Cypress matches the most recently registered intercept first, so any spec intercept on that route shadows it — the test then fails withIntercepted Gainsight API key call, but Gainsight should have been disabled. Whenever you interceptcurrentTenant, re-setcustomProperties.gainsightEnabled = falsein your handler (as above).
Selector priority
data-cyattributes (add them to the component under test if missing — that is part of the task).- Cumulocity component tags:
c8y-data-grid,c8y-ui-empty-state,c8y-alert-outlet,.modal-content, and built-indata-cyhooks. Knownc8y-data-gridhooks:button[data-cy="data-grid--reload-btn"],td[data-cy="data-grid--"](uses the display header, e.g.data-grid--Last Updated),c8y-data-grid--edit-button-in-row,c8y-data-grid--remove-button-in-row,c8y-confirm-modal--ok. aria-label/role, orspan[title="…"]for tab items (c8y-tabs-outlet span[title=""]).- Never: positional selectors, generated class names, text-only matching for critical steps (text is locale-dependent — the suite runs with
language: 'en').
All selectors should come from the Phase 1a live-DOM walk (or its fallbacks) — never from memory or from grepping minified bundles.
Widget gotchas (verified against ngx-components 1023.x / ngx-bootstrap):
c8y-data-gridrenders withdisplay: contents→ 0×0 size, so.should('be.visible')on the tag itself always fails. Assert visibility on rendered content inside it (atd[data-cy=…]cell) instead.- ngx-bootstrap tooltips open on
mouseover(notmouseenter):.trigger('mouseover'), then assertcy.get('body .tooltip')(tooltips withcontainer="body"render at the body level, outside any.within()scope).
Useful cumulocity-cypress helpers
cy.getAuth(), .login(), .disableGainsight(), cy.setLanguage('en'), cy.compareDates(displayedText, isoString) for locale-formatted dates in grids.
Phase 3 — Verify in a loop until green
- Run only the spec you changed, headless, capturing full output to a file — never pipe through
head/tail, truncation destroys the diagnosis:
``bash npx cypress run --browser chrome --spec cypress/e2e/.cy.ts > /path/to/scratch/run.log 2>&1 `` Run it in the background if it takes minutes; a shell-based spec typically takes 1–3 min.
- On failure, read the trace, not just the error. With
cypress-terminal-reportinstalled, the log prints every request and command:
cy:fetch/cy:xhrlines = requests the app actually made. A request matched by one of your intercepts is prefixed with the alias and markedSTUBBED(e.g.cy:fetch ➟ (getConfiguration) STUBBED GET …); a line without that prefix passed through to the real backend.Matcher: "…"lines = what yourcy.waitwas waiting for.- Compare the two — a mismatch is almost always the bug.
- Failure signature table:
| Symptom | Likely cause | |---|---| | cy.wait() timed out … No request ever occurred | Intercept URL doesn't match reality (check service code), or intercept registered after the request fired | | before each hook fails on cy.request() to /tenant/oauth with 400 A tenant is required in context! | C8Y_TENANT doesn't belong to C8Y_BASEURL — rerun preflight step 3 | | Anchor selector never appears after visit | Wrong C8Y_SHELL_TARGET, shell app not deployed on tenant, or plugin remotes not in the query string | | Element not found inside plugin | Plugin failed to load (check for module-federation errors in the log) or missing data-cy | | Grid shows data but assertion on time/date fails | Locale formatting — use cy.compareDates | | Intercepted Gainsight API key call, but Gainsight should have been disabled | A spec intercept on /tenant/currentTenant* shadowed the disableGainsight() intercept — re-set gainsightEnabled: false in your handler (intercept rule 9) | | expected '' to be 'visible' with effective width and height of 0 x 0 | The component uses display: contents — assert on rendered content inside it | | Tooltip/popover never appears after .trigger('mouseenter') | ngx-bootstrap listens to mouseover — trigger that instead |
- Fix, rerun the single spec, repeat. When green, run the full suite once (the repo's e2e script, or
npx cypress run --browser chrome) to catch cross-spec interference (sharedC8Y_SHELL_TARGET, session state). - Report results with the pass/fail counts from the summary table — never claim success without a green run.
Security constraints
- Never print
.envvalues, tokens, orAuthorizationheaders. When curling with credentials, output status codes only. - A failed login makes Cypress dump the full request body — including the password — into the run log. Treat every run log as sensitive: keep them in the scratchpad, delete them when done, never commit them, and never paste their login sections into summaries, commits, or PRs.
- To verify whether a log leaked the password without printing it, compare counts only:
``bash PW=$(grep '^C8Y_PASSWORD=' .env | cut -d= -f2- | tr -d '\r') grep -cF -- "$PW" run.log # 0 = clean ``
- If a CI log has leaked a credential this way, tell the user and recommend rotating it.
Definition of done
- [ ] Preflight passed (server up, tenant verified, device valid if needed)
- [ ] Every intercept URL cross-checked against the current service source
- [ ] Selectors taken from the live DOM (or its fallbacks), not from memory
- [ ] All mutating requests intercepted; request bodies asserted
- [ ] Success and failure paths covere
…
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Cumulocity-IoT
- Source: Cumulocity-IoT/cumulocity-skills-community
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.