docs: Add CLAUDE.md documentation for key .NET projects (#20841)
* docs: Add CLAUDE.md documentation for key .NET projects Add comprehensive CLAUDE.md files for major Umbraco projects: - Root CLAUDE.md: Multi-project repository overview - Umbraco.Core: Interface contracts and domain models - Umbraco.Infrastructure: Implementation layer (NPoco, migrations, services) - Umbraco.Cms.Api.Common: Shared API infrastructure - Umbraco.Cms.Api.Management: Management API (1,317 files, 54 domains) - Umbraco.Web.UI.Client: Frontend with split docs structure Each file includes: - Architecture and design patterns - Project-specific workflows - Edge cases and gotchas - Commands and setup - Technical debt tracking 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update src/Umbraco.Cms.Api.Management/CLAUDE.md Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> * Apply suggestions from code review Co-authored-by: Andy Butland <abutland73@gmail.com> Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> * docs: Update CLAUDE.md with accurate persistence and auth info - Clarify NPoco is current and fully supported (not legacy) - Document EF Core as future direction with ongoing migration - Add secure cookie-based token storage details for v17+ - Update OpenIddict authentication documentation - Update API versioning (v1.0 and v1.1) - Minor documentation cleanups (community links, descriptions) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * Update src/Umbraco.Web.UI.Client/docs/agentic-workflow.md * Update src/Umbraco.Web.UI.Client/docs/agentic-workflow.md * Apply suggestions from code review * Clarifications and duplicate removal. --------- Co-authored-by: Phil Whittaker <pjw@umbraco.dk> Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Co-authored-by: Andy Butland <abutland73@gmail.com> Co-authored-by: Sven Geusens <geusens@gmail.com>
This commit is contained in:
89
src/Umbraco.Web.UI.Client/CLAUDE.md
Normal file
89
src/Umbraco.Web.UI.Client/CLAUDE.md
Normal file
@@ -0,0 +1,89 @@
|
||||
# Umbraco Backoffice - @umbraco-cms/backoffice
|
||||
|
||||
Modern TypeScript/Lit-based web components library for the Umbraco CMS backoffice interface. This project provides extensible UI components, APIs, and utilities for building the Umbraco CMS administration interface.
|
||||
|
||||
**Package**: `@umbraco-cms/backoffice`
|
||||
**Version**: 17.1.0-rc
|
||||
**License**: MIT
|
||||
**Repository**: https://github.com/umbraco/Umbraco-CMS
|
||||
**Live Preview**: https://backofficepreview.umbraco.com/
|
||||
|
||||
---
|
||||
|
||||
## Documentation Structure
|
||||
|
||||
This project's documentation is organized into 9 focused guides:
|
||||
|
||||
**Note**: This is a sub-project in the Umbraco CMS monorepo. For Git workflow, PR process, and CI/CD information, see the [repository root CLAUDE.md](../../CLAUDE.md).
|
||||
|
||||
### Architecture & Design
|
||||
- **[Architecture](./docs/architecture.md)** - Technology stack, design patterns, module organization
|
||||
|
||||
### Development
|
||||
- **[Commands](./docs/commands.md)** - Build, test, and development commands
|
||||
|
||||
### Code Quality
|
||||
- **[Style Guide](./docs/style-guide.md)** - Naming and formatting conventions
|
||||
- **[Clean Code](./docs/clean-code.md)** - Best practices and SOLID principles
|
||||
- **[Testing](./docs/testing.md)** - Unit, integration, and E2E testing strategies
|
||||
|
||||
### Troubleshooting
|
||||
- **[Error Handling](./docs/error-handling.md)** - Error patterns and debugging
|
||||
- **[Edge Cases](./docs/edge-cases.md)** - Common pitfalls and gotchas
|
||||
|
||||
### Security & AI
|
||||
- **[Security](./docs/security.md)** - XSS prevention, authentication, input validation
|
||||
- **[Agentic Workflow](./docs/agentic-workflow.md)** - Three-phase AI development process
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- **Node.js**: >=22.17.1
|
||||
- **npm**: >=10.9.2
|
||||
- Modern browser (Chrome, Firefox, Safari)
|
||||
|
||||
### Initial Setup
|
||||
|
||||
```bash
|
||||
# 1. Clone repository
|
||||
git clone https://github.com/umbraco/Umbraco-CMS.git
|
||||
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
|
||||
|
||||
# 2. Install dependencies
|
||||
npm install
|
||||
|
||||
# 3. Start development
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### Most Common Commands
|
||||
|
||||
See **[Commands](./docs/commands.md)** for all available commands.
|
||||
|
||||
**Development**: `npm run dev` | **Testing**: `npm test` | **Build**: `npm run build` | **Lint**: `npm run lint:fix`
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Category | Details |
|
||||
|----------|---------|
|
||||
| **Apps** | `src/apps/` - Application entry points (app, installer, upgrader) |
|
||||
| **Libraries** | `src/libs/` - Core APIs (element-api, context-api, controller-api) |
|
||||
| **Packages** | `src/packages/` - Feature packages; `src/packages/core/` for utilities |
|
||||
| **External** | `src/external/` - Dependency wrappers (lit, rxjs, luxon) |
|
||||
| **Mocks** | `src/mocks/` - MSW handlers and mock data |
|
||||
| **Config** | `package.json`, `vite.config.ts`, `.env` (create `.env.local`) |
|
||||
| **Elements** | Custom elements use `umb-{feature}-{component}` pattern |
|
||||
|
||||
### Getting Help
|
||||
|
||||
**Documentation**: [UI API Docs](npm run generate:ui-api-docs) | [Storybook](npm run storybook) | [Official Docs](https://docs.umbraco.com/)
|
||||
**Community**: [Issues](https://github.com/umbraco/Umbraco-CMS/issues) | [Discussions](https://github.com/umbraco/Umbraco-CMS/discussions) | [Forum](https://our.umbraco.com/)
|
||||
|
||||
---
|
||||
|
||||
**This project follows a modular package architecture with strict TypeScript, Lit web components, and an extensible manifest system. Each package is independent but follows consistent patterns. For extension development, use the Context API for dependency injection, controllers for logic, and manifests for registration.**
|
||||
517
src/Umbraco.Web.UI.Client/docs/agentic-workflow.md
Normal file
517
src/Umbraco.Web.UI.Client/docs/agentic-workflow.md
Normal file
@@ -0,0 +1,517 @@
|
||||
# Agentic Workflow
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: Analysis & Planning
|
||||
|
||||
**Understand Requirements**:
|
||||
1. Read user request carefully
|
||||
2. Identify acceptance criteria
|
||||
3. Ask clarifying questions if ambiguous
|
||||
4. Determine scope (new feature, bug fix, refactor, etc.)
|
||||
|
||||
**Research Codebase**:
|
||||
1. Find similar implementations in codebase
|
||||
2. Identify patterns and conventions used
|
||||
3. Locate relevant packages and modules
|
||||
4. Review existing tests for similar features
|
||||
5. Check for existing utilities or helpers
|
||||
|
||||
**Identify Technical Approach**:
|
||||
1. Which packages need changes?
|
||||
- New package or modify existing?
|
||||
- Core infrastructure or feature package?
|
||||
2. What patterns to use?
|
||||
- Web Component, Controller, Repository, Context?
|
||||
- Extension type? (dashboard, workspace, modal, etc.)
|
||||
3. Dependencies needed?
|
||||
- New npm packages?
|
||||
- Internal package dependencies?
|
||||
4. API changes needed?
|
||||
- New OpenAPI endpoints?
|
||||
- Changes to existing endpoints?
|
||||
|
||||
**Break Down Implementation**:
|
||||
1. **Models/Types** - Define TypeScript interfaces and types
|
||||
2. **API Client** - Update OpenAPI client if backend changes
|
||||
3. **Repository** - Data access layer
|
||||
4. **Store/Context** - State management
|
||||
5. **Controller** - Business logic
|
||||
6. **Element** - UI component
|
||||
7. **Manifest** - Extension registration
|
||||
8. **Tests** - Unit and integration tests
|
||||
9. **Documentation** - JSDoc and examples
|
||||
10. **Storybook** (if applicable) - Component stories
|
||||
|
||||
**Consider Architecture**:
|
||||
- Does this follow project patterns?
|
||||
- Are dependencies correct? (libs → packages → apps)
|
||||
- Will this create circular dependencies?
|
||||
- Is this extensible for future needs?
|
||||
- Performance implications?
|
||||
|
||||
**Document Plan**:
|
||||
- Write brief implementation plan
|
||||
- Identify potential issues or blockers
|
||||
- Get approval from user if significant changes
|
||||
|
||||
### Phase 2: Incremental Implementation
|
||||
|
||||
**For New Feature (Component/Package)**:
|
||||
|
||||
**Step 1: Define Types**
|
||||
```typescript
|
||||
// 1. Create model interface
|
||||
export interface UmbMyModel {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
// 2. Create manifest type
|
||||
export interface UmbMyManifest extends UmbManifestBase {
|
||||
type: 'myType';
|
||||
// ... specific properties
|
||||
}
|
||||
```
|
||||
|
||||
**Verify**: TypeScript compiles, no errors
|
||||
|
||||
**Step 2: Create Repository**
|
||||
```typescript
|
||||
// 3. Data access layer
|
||||
export class UmbMyRepository {
|
||||
async requestById(id: string) {
|
||||
// Fetch from API
|
||||
// Return { data } or { error }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verify**: Repository compiles, basic structure correct
|
||||
|
||||
**Step 3: Create Store/Context**
|
||||
```typescript
|
||||
// 4. State management
|
||||
export class UmbMyStore extends UmbStoreBase {
|
||||
// Observable state
|
||||
}
|
||||
|
||||
export const UMB_MY_CONTEXT = new UmbContextToken<UmbMyContext>('UmbMyContext');
|
||||
|
||||
export class UmbMyContext extends UmbControllerBase {
|
||||
#repository = new UmbMyRepository();
|
||||
#store = new UmbMyStore(this);
|
||||
|
||||
// Public API
|
||||
}
|
||||
```
|
||||
|
||||
**Verify**: Context compiles, can be consumed
|
||||
|
||||
**Step 4: Create Element**
|
||||
```typescript
|
||||
// 5. UI component
|
||||
@customElement('umb-my-element')
|
||||
export class UmbMyElement extends UmbLitElement {
|
||||
#context?: typeof UMB_MY_CONTEXT.TYPE;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.consumeContext(UMB_MY_CONTEXT, (context) => {
|
||||
this.#context = context;
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<div>My Element</div>`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verify**: Element renders in browser, no console errors
|
||||
|
||||
**Step 5: Wire Up Interactions**
|
||||
```typescript
|
||||
// 6. Connect user interactions to logic
|
||||
@customElement('umb-my-element')
|
||||
export class UmbMyElement extends UmbLitElement {
|
||||
@state()
|
||||
private _data?: UmbMyModel;
|
||||
|
||||
async #handleClick() {
|
||||
const { data, error } = await this.#context?.load();
|
||||
if (error) {
|
||||
this._error = error;
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`
|
||||
<uui-button @click=${this.#handleClick} label="Load"></uui-button>
|
||||
${this._data ? html`<p>${this._data.name}</p>` : nothing}
|
||||
`;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Verify**: Interactions work, data flows correctly
|
||||
|
||||
**Step 6: Add Extension Manifest**
|
||||
```typescript
|
||||
// 7. Register extension
|
||||
export const manifest: UmbManifestMyType = {
|
||||
type: 'myType',
|
||||
alias: 'My.Extension',
|
||||
name: 'My Extension',
|
||||
element: () => import('./my-element.element.js'),
|
||||
};
|
||||
```
|
||||
|
||||
**Verify**: Extension loads, manifest is valid
|
||||
|
||||
**Step 7: Write Tests**
|
||||
```typescript
|
||||
// 8. Unit tests
|
||||
describe('UmbMyElement', () => {
|
||||
it('should render', async () => {
|
||||
const element = await fixture(html`<umb-my-element></umb-my-element>`);
|
||||
expect(element).to.exist;
|
||||
});
|
||||
|
||||
it('should load data', async () => {
|
||||
// Test data loading
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**Verify**: Tests pass
|
||||
|
||||
**Step 8: Add Error Handling**
|
||||
```typescript
|
||||
// 9. Handle errors gracefully
|
||||
async #handleClick() {
|
||||
try {
|
||||
this._loading = true;
|
||||
const { data, error } = await this.#context?.load();
|
||||
if (error) {
|
||||
this._error = 'Failed to load data';
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
} catch (error) {
|
||||
this._error = 'Unexpected error occurred';
|
||||
console.error('Load failed:', error);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 9: Add localization**
|
||||
```typescript
|
||||
// 10. Add to src/Umbraco.Web.UI.Client/src/assets/lang/en.ts and other appropriate files
|
||||
{
|
||||
actions: {
|
||||
load: 'Load'
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Use the localize helper (`this.localize.term()`) in the element
|
||||
render() {
|
||||
return html`
|
||||
<uui-button @click=${this.#handleClick} label=${this.localize.term('actions_load')></uui-button>
|
||||
${this._data ? html`<p>${this._data.name}</p>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
async #handleClick() {
|
||||
try {
|
||||
this._loading = true;
|
||||
const { data, error } = await this.#context?.load();
|
||||
if (error) {
|
||||
this._error = this.localize.term('errors_receivedErrorFromServer');
|
||||
return;
|
||||
}
|
||||
this._data = data;
|
||||
} catch (error) {
|
||||
this._error = this.localize.term('errors_defaultError');
|
||||
console.error('Load failed:', error);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 12. Outside elements (such as controllers), use the Localization Controller
|
||||
export class UmbMyController extends UmbControllerBase {
|
||||
#localize = new UmbLocalizationController(this);
|
||||
#notificationContext?: typeof UMB_NOTIFICATION_CONTEXT.TYPE;
|
||||
|
||||
constructor(host: UmbControllerHost) {
|
||||
super(host);
|
||||
|
||||
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => {
|
||||
this.#notificationContext = notificationContext;
|
||||
});
|
||||
}
|
||||
|
||||
fetchData() {
|
||||
// Show error
|
||||
this.#notificationContext?.peek('positive', {
|
||||
data: {
|
||||
headline: this.#localize.term('speechBubbles_onlineHeadline'),
|
||||
message: this.#localize.term('speechBubbles_onlineMessage'),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
**Verify**: Errors are handled, UI shows error state
|
||||
|
||||
**After Each Step**:
|
||||
- ✅ Code compiles (no TypeScript errors)
|
||||
- ✅ Tests pass (existing and new)
|
||||
- ✅ Follows patterns (consistent with codebase)
|
||||
- ✅ No breaking changes (or documented)
|
||||
- ✅ ESLint passes
|
||||
- ✅ Commit working code
|
||||
|
||||
### Phase 3: Review & Refinement
|
||||
|
||||
**Run All Quality Checks**:
|
||||
|
||||
```bash
|
||||
# 1. Run all tests
|
||||
npm test
|
||||
|
||||
# 2. Run lint
|
||||
npm run lint:errors
|
||||
|
||||
# 3. Type check
|
||||
npm run compile
|
||||
|
||||
# 4. Build
|
||||
npm run build
|
||||
|
||||
# 5. Check circular dependencies
|
||||
npm run check:circular
|
||||
|
||||
# 6. Validate package exports
|
||||
npm run package:validate
|
||||
```
|
||||
|
||||
**Code Review Checklist**:
|
||||
- [ ] Code follows style guide
|
||||
- [ ] All tests pass
|
||||
- [ ] New tests added for new code
|
||||
- [ ] TypeScript types are correct
|
||||
- [ ] No `any` types (or justified)
|
||||
- [ ] JSDoc comments on public APIs
|
||||
- [ ] Error handling in place
|
||||
- [ ] Edge cases handled
|
||||
- [ ] No console.log in production code
|
||||
- [ ] Accessibility considered (for UI components)
|
||||
- [ ] Performance acceptable
|
||||
- [ ] No memory leaks (subscriptions cleaned up)
|
||||
- [ ] Follows existing patterns
|
||||
- [ ] No circular dependencies introduced
|
||||
|
||||
**Documentation**:
|
||||
- [ ] JSDoc on public APIs
|
||||
- [ ] Update README if needed
|
||||
- [ ] Add Storybook story (for components)
|
||||
- [ ] Add example code (for APIs)
|
||||
- [ ] Update CLAUDE.md (for significant changes)
|
||||
|
||||
### Reasoning & Decision-Making
|
||||
|
||||
**Architecture Decisions**:
|
||||
- **Follow existing patterns** - Don't invent new patterns unless necessary
|
||||
- **Use Context API** - For cross-component state and dependencies
|
||||
- **Controllers for logic** - Separate business logic from UI
|
||||
- **Repositories for data** - Abstract data access
|
||||
- **Manifests for extensions** - Register features via extension system
|
||||
|
||||
**Component Design Decisions**:
|
||||
- **Small components** - Break complex UI into smaller components
|
||||
- **Single responsibility** - Each component does one thing well
|
||||
- **Composition** - Combine small components to build complex UI
|
||||
- **Props down, events up** - Data flows down via properties, changes flow up via events
|
||||
|
||||
**State Management Decisions**:
|
||||
- **Local state** - `@state()` for component-specific state
|
||||
- **Shared state** - Context and stores for cross-component state
|
||||
- **Observables** - RxJS for reactive data streams
|
||||
- **Immutability** - Don't mutate state, create new objects
|
||||
|
||||
**API Design Decisions**:
|
||||
- **Async by default** - All data operations are async
|
||||
- **Error objects** - Return `{ data, error }` not throw exceptions
|
||||
- **TypeScript types** - Strong typing for all APIs
|
||||
- **Consistent naming** - Follow existing naming conventions
|
||||
|
||||
### Think-Aloud Process
|
||||
|
||||
**Example: "I need to add a bulk action for content"**
|
||||
|
||||
**Analysis**:
|
||||
> "I need to add a bulk action for content. Let me think through this:
|
||||
>
|
||||
> 1. What is a bulk action? It's an action that operates on multiple items.
|
||||
> 2. Where do bulk actions appear? In collection views (document tree, list view).
|
||||
> 3. What pattern is used? Let me check existing bulk actions...
|
||||
> 4. Found: Block editor has bulk actions. Let me review that code.
|
||||
> 5. Architecture: Bulk actions are registered via manifests (type: 'entityBulkAction')
|
||||
> 6. Implementation needs:
|
||||
> - Manifest to register action
|
||||
> - Element to show confirmation dialog
|
||||
> - Repository method to perform bulk operation
|
||||
> - Permission check (can user do this?)
|
||||
> 7. Which package? This goes in `packages/documents/documents/` since it's document-specific.
|
||||
> 8. Dependencies: Need `@umbraco-cms/backoffice/entity-bulk-action`, modal, repository."
|
||||
|
||||
**Planning**:
|
||||
> "Implementation steps:
|
||||
> 1. Create bulk action manifest in documents package
|
||||
> 2. Create modal element for confirmation
|
||||
> 3. Add repository method for bulk operation
|
||||
> 4. Add permission check
|
||||
> 5. Wire up action to modal
|
||||
> 6. Test with multiple documents
|
||||
> 7. Handle errors (partial success, permissions, etc.)
|
||||
> 8. Add loading state
|
||||
> 9. Show success/error notification
|
||||
> 10. Write tests"
|
||||
|
||||
**Implementation**:
|
||||
> "Starting with step 1: Create manifest.
|
||||
> Looking at existing bulk actions, I see the pattern uses `UmbEntityBulkActionBase`.
|
||||
> Let me create the manifest following that pattern..."
|
||||
|
||||
### Error Recovery
|
||||
|
||||
**If Tests Fail**:
|
||||
1. Read the error message carefully
|
||||
2. Reproduce the failure locally
|
||||
3. Debug with console.log or debugger
|
||||
4. Understand root cause (don't just fix symptoms)
|
||||
5. Fix the underlying issue
|
||||
6. Verify fix doesn't break other tests
|
||||
7. Add test for the bug (if it was a real bug)
|
||||
|
||||
**If Architecture Feels Wrong**:
|
||||
1. Pause and reconsider
|
||||
2. Review similar implementations in codebase
|
||||
3. Discuss with team (via PR comments)
|
||||
4. Don't force a pattern that doesn't fit
|
||||
5. Refactor if needed
|
||||
|
||||
**If Introducing Breaking Changes**:
|
||||
1. Discuss with user first
|
||||
2. Document the breaking change
|
||||
3. Provide migration path
|
||||
4. Update CHANGELOG
|
||||
5. Consider deprecation instead
|
||||
|
||||
**If Stuck**:
|
||||
1. Ask for clarification from user
|
||||
2. Review documentation
|
||||
3. Look at similar code in repository
|
||||
4. Break problem into smaller pieces
|
||||
5. Try a simpler approach first
|
||||
|
||||
### Quality Gates Checklist
|
||||
|
||||
Before marking work as complete:
|
||||
|
||||
**Code Quality**:
|
||||
- [ ] All new code has unit tests
|
||||
- [ ] Integration tests for workflows (if applicable)
|
||||
- [ ] All tests pass (including existing tests)
|
||||
- [ ] Code follows style guide (ESLint passes)
|
||||
- [ ] Prettier formatting correct
|
||||
- [ ] No TypeScript errors
|
||||
- [ ] JSDoc comments on public functions
|
||||
- [ ] No `console.log` in production code
|
||||
- [ ] No commented-out code
|
||||
|
||||
**Security**:
|
||||
- [ ] Input validation in place
|
||||
- [ ] No XSS vulnerabilities
|
||||
- [ ] No sensitive data logged
|
||||
- [ ] User input sanitized
|
||||
- [ ] Permissions checked (for sensitive operations)
|
||||
|
||||
**Performance**:
|
||||
- [ ] No obvious performance issues
|
||||
- [ ] No memory leaks (subscriptions cleaned up)
|
||||
- [ ] Efficient algorithms used
|
||||
- [ ] Large lists use virtualization (if needed)
|
||||
|
||||
**Architecture**:
|
||||
- [ ] Follows existing patterns
|
||||
- [ ] No circular dependencies
|
||||
- [ ] Correct package dependencies
|
||||
- [ ] Extension manifest correct (if applicable)
|
||||
|
||||
**Documentation**:
|
||||
- [ ] JSDoc on public APIs
|
||||
- [ ] README updated (if needed)
|
||||
- [ ] Examples added (if new API)
|
||||
- [ ] Breaking changes documented
|
||||
|
||||
**User Experience** (for UI changes):
|
||||
- [ ] Accessible (keyboard navigation, screen readers)
|
||||
- [ ] Responsive (works on different screen sizes)
|
||||
- [ ] Loading states shown
|
||||
- [ ] Errors handled gracefully
|
||||
- [ ] Success feedback provided
|
||||
|
||||
### Communication
|
||||
|
||||
**After Each Phase**:
|
||||
- Summarize what was implemented
|
||||
- Highlight key decisions and why
|
||||
- Call out any blockers or questions
|
||||
- Show progress (code snippets, screenshots for UI)
|
||||
|
||||
**When Making Decisions**:
|
||||
- Explain reasoning
|
||||
- Reference existing patterns
|
||||
- Call out trade-offs
|
||||
- Ask for confirmation on significant changes
|
||||
|
||||
**When Blocked**:
|
||||
- Clearly state the blocker
|
||||
- Explain what you've tried
|
||||
- Ask specific questions
|
||||
- Suggest potential approaches
|
||||
|
||||
**When Complete**:
|
||||
- Summarize what was delivered
|
||||
- Note any deviations from plan (and why)
|
||||
- Highlight testing performed
|
||||
- Mention any follow-up work needed
|
||||
|
||||
### For Large Features
|
||||
|
||||
**Multi-Session Features**:
|
||||
1. Create feature branch (if spanning multiple PRs)
|
||||
2. Break into multiple PRs if very large:
|
||||
- PR 1: Infrastructure (types, repository, context)
|
||||
- PR 2: UI components
|
||||
- PR 3: Extensions and integration
|
||||
3. Keep main stable - only merge completed, tested features
|
||||
4. Consider feature flags for gradual rollout
|
||||
5. Document progress and remaining work in PR description
|
||||
|
||||
**Coordination**:
|
||||
- Update PR description with progress
|
||||
- Use GitHub task lists in PR description
|
||||
- Comment on PR with status updates
|
||||
- Request early feedback on architecture
|
||||
|
||||
---
|
||||
|
||||
124
src/Umbraco.Web.UI.Client/docs/architecture.md
Normal file
124
src/Umbraco.Web.UI.Client/docs/architecture.md
Normal file
@@ -0,0 +1,124 @@
|
||||
# Architecture
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Technology Stack
|
||||
|
||||
- **Node.js**: >=22.17.1
|
||||
- **npm**: >=10.9.2
|
||||
- **Language**: TypeScript 5.9.3
|
||||
- **Module System**: ESM (ES Modules)
|
||||
- **Framework**: Lit (Web Components)
|
||||
- **Build Tool**: Vite 7.1.11
|
||||
- **Test Framework**: @web/test-runner with Playwright
|
||||
- **E2E Testing**: Playwright 1.55.1
|
||||
- **Code Quality**: ESLint 9.37.0, Prettier 3.6.2
|
||||
- **Mocking**: MSW (Mock Service Worker) 1.3.5
|
||||
- **Documentation**: Storybook 9.0.14, TypeDoc 0.28.13
|
||||
|
||||
### Application Type
|
||||
|
||||
Single-page web application (SPA) packaged as an npm library called `@umbraco-cms/backoffice` for typings and built as a bundle, which is copied over to `src/Umbraco.Cms.StaticAssets/wwwroot/umbraco/backoffice` for the CMS. Provides:
|
||||
- Extensible Web Components for CMS backoffice UI
|
||||
- API libraries for extension development
|
||||
- TypeScript type definitions
|
||||
- Package manifest system for extensions
|
||||
|
||||
### Architecture Pattern
|
||||
|
||||
**Modular Package Architecture** with clear separation:
|
||||
|
||||
```
|
||||
src/
|
||||
├── apps/ # Application entry points
|
||||
│ ├── app/ # Main backoffice application
|
||||
│ ├── backoffice/ # Backoffice shell
|
||||
│ ├── installer/ # CMS installer interface
|
||||
│ ├── preview/ # Content preview
|
||||
│ └── upgrader/ # CMS upgrader interface
|
||||
│
|
||||
├── libs/ # Core API libraries (infrastructure)
|
||||
│ ├── class-api/ # Base class utilities
|
||||
│ ├── context-api/ # Context API for dependency injection
|
||||
│ ├── context-proxy/ # Context proxying utilities
|
||||
│ ├── controller-api/ # Controller lifecycle management
|
||||
│ ├── element-api/ # Element base classes & mixins
|
||||
│ ├── extension-api/ # Extension registration & loading
|
||||
│ ├── localization-api/ # Internationalization
|
||||
│ └── observable-api/ # Reactive state management
|
||||
│
|
||||
├── packages/ # Feature packages (50+ packages)
|
||||
│ ├── core/ # Core utilities (auth, http, router, etc.)
|
||||
│ ├── content/ # Content management
|
||||
│ ├── documents/ # Document types & editing
|
||||
│ ├── media/ # Media management
|
||||
│ ├── members/ # Member management
|
||||
│ ├── user/ # User management
|
||||
│ ├── templating/ # Templates, scripts, stylesheets
|
||||
│ ├── block/ # Block editor components
|
||||
│ └── ... # 30+ more specialized packages
|
||||
│
|
||||
├── external/ # External dependency wrappers
|
||||
│ ├── lit/ # Lit framework wrapper
|
||||
│ ├── rxjs/ # RxJS wrapper
|
||||
│ ├── luxon/ # Date/time library wrapper
|
||||
│ ├── monaco-editor/# Code editor wrapper
|
||||
│ └── ... # Other wrapped dependencies
|
||||
│
|
||||
├── mocks/ # MSW mock handlers & test data
|
||||
│ ├── data/ # Mock database
|
||||
│ └── handlers/ # API request handlers
|
||||
│
|
||||
└── assets/ # Static assets (fonts, images, localization)
|
||||
```
|
||||
|
||||
### Design Patterns
|
||||
|
||||
1. **Web Components** - Custom elements with Shadow DOM encapsulation
|
||||
2. **Context API** - Dependency injection via context providers/consumers (similar to React Context)
|
||||
3. **Controller Pattern** - Lifecycle-aware controllers for managing component behavior
|
||||
4. **Extension System** - Manifest-based plugin architecture
|
||||
5. **Observable Pattern** - Reactive state management with RxJS observables
|
||||
6. **Repository Pattern** - Data access abstraction via repository classes
|
||||
7. **Mixin Pattern** - Composable behaviors via TypeScript mixins (`UmbElementMixin`, etc.)
|
||||
8. **Builder Pattern** - For complex object construction
|
||||
9. **Registry Pattern** - Extension registry for dynamic feature loading
|
||||
10. **Observer Pattern** - Event-driven communication between components
|
||||
|
||||
### Key Technologies
|
||||
|
||||
**Core Framework**:
|
||||
- Lit 3.x - Web Components framework with reactive templates
|
||||
- TypeScript 5.9 - Type-safe development with strict mode
|
||||
- Vite - Fast build tool and dev server
|
||||
|
||||
**UI Components**:
|
||||
- @umbraco-ui/uui - Umbraco UI component library
|
||||
- Shadow DOM - Component style encapsulation
|
||||
- Custom Elements API - Native web components
|
||||
|
||||
**State & Data**:
|
||||
- RxJS - Reactive programming with observables
|
||||
- Context API - State management & dependency injection
|
||||
- MSW - API mocking for development & testing
|
||||
|
||||
**Testing**:
|
||||
- @web/test-runner - Fast test runner for web components
|
||||
- Playwright - E2E browser testing
|
||||
- @open-wc/testing - Testing utilities for web components
|
||||
|
||||
**Code Quality**:
|
||||
- ESLint with TypeScript plugin - Linting with strict rules
|
||||
- Prettier - Code formatting
|
||||
- TypeDoc - API documentation generation
|
||||
- Web Component Analyzer - Custom element documentation
|
||||
|
||||
**Other**:
|
||||
- Luxon - Date/time manipulation
|
||||
- Monaco Editor - Code editing
|
||||
- DOMPurify - HTML sanitization
|
||||
- Marked - Markdown parsing
|
||||
- SignalR - Real-time communication
|
||||
|
||||
397
src/Umbraco.Web.UI.Client/docs/clean-code.md
Normal file
397
src/Umbraco.Web.UI.Client/docs/clean-code.md
Normal file
@@ -0,0 +1,397 @@
|
||||
# Clean Code
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Function Design
|
||||
|
||||
**Function Length**:
|
||||
- Target: ≤30 lines per function
|
||||
- Max: 50 lines (enforce via code review)
|
||||
- Extract complex logic to separate functions
|
||||
- Use early returns to reduce nesting
|
||||
|
||||
**Single Responsibility**:
|
||||
```typescript
|
||||
// Good - Single responsibility
|
||||
private _validateName(name: string): boolean {
|
||||
return name.length > 0 && name.length <= 100;
|
||||
}
|
||||
|
||||
private _sanitizeName(name: string): string {
|
||||
return name.trim().replace(/[<>]/g, '');
|
||||
}
|
||||
|
||||
// Bad - Multiple responsibilities
|
||||
private _processName(name: string): { valid: boolean; sanitized: string } {
|
||||
// Doing too much in one function
|
||||
}
|
||||
```
|
||||
|
||||
**Descriptive Names**:
|
||||
- Use verb names for functions: `loadData`, `validateInput`, `handleClick`
|
||||
- Boolean functions: `is`, `has`, `can`, `should` prefix
|
||||
- Event handlers: `handle`, `on` prefix: `handleSubmit`, `onClick`
|
||||
|
||||
**Parameters**:
|
||||
- Limit to 3-4 parameters
|
||||
- Use options object for more parameters:
|
||||
|
||||
```typescript
|
||||
// Good - Options object
|
||||
interface LoadOptions {
|
||||
id: string;
|
||||
includeChildren?: boolean;
|
||||
depth?: number;
|
||||
culture?: string;
|
||||
}
|
||||
|
||||
private _load(options: LoadOptions) { }
|
||||
|
||||
// Bad - Too many parameters
|
||||
private _load(id: string, includeChildren: boolean, depth: number, culture: string) { }
|
||||
```
|
||||
|
||||
**Early Returns**:
|
||||
|
||||
```typescript
|
||||
// Good - Early returns reduce nesting
|
||||
private _validate(): boolean {
|
||||
if (!this._data) return false;
|
||||
if (!this._data.name) return false;
|
||||
if (this._data.name.length === 0) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Bad - Nested conditions
|
||||
private _validate(): boolean {
|
||||
if (this._data) {
|
||||
if (this._data.name) {
|
||||
if (this._data.name.length > 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
### Class Design
|
||||
|
||||
**Single Responsibility**:
|
||||
- Each class should have one reason to change
|
||||
- Controllers handle one aspect of behavior
|
||||
- Repositories handle one entity type
|
||||
- Components handle one UI concern
|
||||
|
||||
**Small Classes**:
|
||||
- Target: <300 lines per class
|
||||
- Extract complex logic to separate controllers/utilities
|
||||
- Use composition over inheritance
|
||||
|
||||
**Encapsulation**:
|
||||
|
||||
```typescript
|
||||
export class UmbMyElement extends LitElement {
|
||||
// Public API - reactive properties
|
||||
@property({ type: String })
|
||||
value = '';
|
||||
|
||||
// Private state
|
||||
@state()
|
||||
private _loading = false;
|
||||
|
||||
// Private fields (not reactive)
|
||||
#controller = new UmbMyController(this);
|
||||
|
||||
// Private methods
|
||||
private _loadData() { }
|
||||
}
|
||||
```
|
||||
|
||||
### SOLID Principles (Adapted for TypeScript/Lit)
|
||||
|
||||
**S - Single Responsibility**:
|
||||
- One component = one UI responsibility
|
||||
- One controller = one behavior responsibility
|
||||
- One repository = one entity type
|
||||
|
||||
**O - Open/Closed**:
|
||||
- Extend via composition and mixins
|
||||
- Extension API for plugins
|
||||
- Avoid modifying existing components, create new ones
|
||||
|
||||
**L - Liskov Substitution**:
|
||||
- Subclasses should honor base class contracts
|
||||
- Use interfaces for polymorphism
|
||||
|
||||
**I - Interface Segregation**:
|
||||
- Small, focused interfaces
|
||||
- Use TypeScript `interface` for contracts
|
||||
|
||||
**D - Dependency Inversion**:
|
||||
- Depend on abstractions (interfaces) not concrete classes
|
||||
- Use Context API for dependency injection
|
||||
- Controllers receive dependencies via constructor
|
||||
|
||||
### Dependency Injection
|
||||
|
||||
**Context API** (Preferred):
|
||||
|
||||
```typescript
|
||||
export class UmbMyElement extends UmbElementMixin(LitElement) {
|
||||
#authContext?: UmbAuthContext;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
// Consume context (dependency injection)
|
||||
this.consumeContext(UMB_AUTH_CONTEXT, (context) => {
|
||||
this.#authContext = context;
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Controller Pattern**:
|
||||
|
||||
```typescript
|
||||
export class UmbMyController extends UmbControllerBase {
|
||||
#repository: UmbContentRepository;
|
||||
|
||||
constructor(host: UmbControllerHost, repository: UmbContentRepository) {
|
||||
super(host);
|
||||
this.#repository = repository;
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
#controller = new UmbMyController(this, new UmbContentRepository());
|
||||
```
|
||||
|
||||
### Avoid Code Smells
|
||||
|
||||
**Magic Numbers/Strings**:
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
if (status === 200) { }
|
||||
if (type === 'document') { }
|
||||
|
||||
// Good
|
||||
const HTTP_OK = 200;
|
||||
const CONTENT_TYPE_DOCUMENT = 'document';
|
||||
|
||||
if (status === HTTP_OK) { }
|
||||
if (type === CONTENT_TYPE_DOCUMENT) { }
|
||||
|
||||
// Or use enums
|
||||
enum ContentType {
|
||||
Document = 'document',
|
||||
Media = 'media',
|
||||
}
|
||||
```
|
||||
|
||||
**Long Parameter Lists**:
|
||||
|
||||
```typescript
|
||||
// Bad
|
||||
function create(name: string, type: string, parent: string, culture: string, template: string) { }
|
||||
|
||||
// Good
|
||||
interface CreateOptions {
|
||||
name: string;
|
||||
type: string;
|
||||
parent?: string;
|
||||
culture?: string;
|
||||
template?: string;
|
||||
}
|
||||
|
||||
function create(options: CreateOptions) { }
|
||||
```
|
||||
|
||||
**Duplicate Code**:
|
||||
- Extract to shared functions
|
||||
- Use composition and mixins
|
||||
- Create utility modules
|
||||
|
||||
**Deeply Nested Code**:
|
||||
- Use early returns
|
||||
- Extract to separate functions
|
||||
- Use guard clauses
|
||||
|
||||
**Callback Hell** (N/A - use async/await)
|
||||
|
||||
### Modern Patterns
|
||||
|
||||
**Async/Await**:
|
||||
|
||||
```typescript
|
||||
// Good - Clean async code
|
||||
async loadContent() {
|
||||
try {
|
||||
this._loading = true;
|
||||
const { data, error } = await this.repository.requestById(this.id);
|
||||
if (error) {
|
||||
this._error = error;
|
||||
return;
|
||||
}
|
||||
this._content = data;
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Optional Chaining**:
|
||||
|
||||
```typescript
|
||||
// Good - Safe property access
|
||||
const name = this._content?.variants?.[0]?.name;
|
||||
|
||||
// Bad - Manual null checks
|
||||
const name = this._content && this._content.variants &&
|
||||
this._content.variants[0] && this._content.variants[0].name;
|
||||
```
|
||||
|
||||
**Destructuring**:
|
||||
|
||||
```typescript
|
||||
// Good - Destructure for clarity
|
||||
const { name, description, icon } = this._content;
|
||||
|
||||
// Good - With defaults
|
||||
const { name = 'Untitled', description = '' } = this._content;
|
||||
```
|
||||
|
||||
**Immutability**:
|
||||
|
||||
```typescript
|
||||
// Good - Spread operator for immutability
|
||||
this._items = [...this._items, newItem];
|
||||
this._config = { ...this._config, newProp: value };
|
||||
|
||||
// Bad - Mutation
|
||||
this._items.push(newItem);
|
||||
this._config.newProp = value;
|
||||
```
|
||||
|
||||
**Pure Functions**:
|
||||
|
||||
```typescript
|
||||
// Good - Pure function (no side effects)
|
||||
function calculateTotal(items: Item[]): number {
|
||||
return items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
|
||||
// Bad - Impure (modifies input)
|
||||
function calculateTotal(items: Item[]): number {
|
||||
items.sort((a, b) => a.price - b.price); // Mutation!
|
||||
return items.reduce((sum, item) => sum + item.price, 0);
|
||||
}
|
||||
```
|
||||
|
||||
### TypeScript-Specific Patterns
|
||||
|
||||
**Type Guards**:
|
||||
|
||||
```typescript
|
||||
function isContentModel(value: unknown): value is UmbContentModel {
|
||||
return typeof value === 'object' && value !== null && 'id' in value;
|
||||
}
|
||||
|
||||
// Usage
|
||||
if (isContentModel(data)) {
|
||||
// TypeScript knows data is UmbContentModel
|
||||
console.log(data.id);
|
||||
}
|
||||
```
|
||||
|
||||
**Discriminated Unions**:
|
||||
|
||||
```typescript
|
||||
type Result<T> =
|
||||
| { success: true; data: T }
|
||||
| { success: false; error: Error };
|
||||
|
||||
function handleResult<T>(result: Result<T>) {
|
||||
if (result.success) {
|
||||
console.log(result.data); // TypeScript knows this exists
|
||||
} else {
|
||||
console.error(result.error); // TypeScript knows this exists
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Utility Types**:
|
||||
|
||||
```typescript
|
||||
// Partial - Make all properties optional
|
||||
type PartialContent = Partial<UmbContentModel>;
|
||||
|
||||
// Pick - Select specific properties
|
||||
type ContentSummary = Pick<UmbContentModel, 'id' | 'name' | 'icon'>;
|
||||
|
||||
// Omit - Remove specific properties
|
||||
type ContentWithoutId = Omit<UmbContentModel, 'id'>;
|
||||
|
||||
// Readonly - Make immutable
|
||||
type ReadonlyContent = Readonly<UmbContentModel>;
|
||||
```
|
||||
|
||||
### Comments and Documentation
|
||||
|
||||
**When to Comment**:
|
||||
- Explain "why" not "what"
|
||||
- Document complex algorithms
|
||||
- JSDoc for public APIs
|
||||
- Warn about gotchas or non-obvious behavior
|
||||
|
||||
**JSDoc for Web Components**:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* A button component that triggers document actions.
|
||||
* @element umb-document-action-button
|
||||
* @fires {CustomEvent} action-click - Fired when action is clicked
|
||||
* @slot - Default slot for button content
|
||||
* @cssprop --umb-button-color - Button text color
|
||||
*/
|
||||
export class UmbDocumentActionButton extends LitElement {
|
||||
/**
|
||||
* The action identifier
|
||||
* @attr
|
||||
* @type {string}
|
||||
*/
|
||||
@property({ type: String })
|
||||
action = '';
|
||||
}
|
||||
```
|
||||
|
||||
**TODOs**:
|
||||
|
||||
```typescript
|
||||
// TODO: Implement pagination [NL]
|
||||
// FIXME: Memory leak in subscription [JOV]
|
||||
// HACK: Temporary workaround for API bug [LK]
|
||||
```
|
||||
|
||||
**Remove Dead Code**:
|
||||
- Don't comment out code, delete it (Git history preserves it)
|
||||
- Remove unused imports, functions, variables
|
||||
- Clean up console.logs before committing
|
||||
|
||||
### Patterns to Avoid
|
||||
|
||||
**Don't**:
|
||||
- Use `var` (use `const`/`let`)
|
||||
- Modify prototypes of built-in objects
|
||||
- Use global variables
|
||||
- Block the main thread (use web workers for heavy computation)
|
||||
- Create deeply nested structures
|
||||
- Use `any` type (use `unknown` or proper types)
|
||||
- Use non-null assertions `!` unless absolutely necessary
|
||||
- Ignore TypeScript errors with `@ts-ignore`
|
||||
|
||||
|
||||
214
src/Umbraco.Web.UI.Client/docs/commands.md
Normal file
214
src/Umbraco.Web.UI.Client/docs/commands.md
Normal file
@@ -0,0 +1,214 @@
|
||||
# Commands
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
# Install dependencies (must use npm, not yarn/pnpm due to workspaces)
|
||||
npm install
|
||||
|
||||
# Note: Requires Node >=22.17.1 and npm >=10.9.2
|
||||
```
|
||||
|
||||
### Build Commands
|
||||
|
||||
```bash
|
||||
# TypeScript compilation only
|
||||
npm run build
|
||||
|
||||
# Build for CMS (production build + copy to .NET project)
|
||||
npm run build:for:cms
|
||||
|
||||
# Build for npm distribution (with type declarations)
|
||||
npm run build:for:npm
|
||||
|
||||
# Build with Vite (alternative build method)
|
||||
npm run build:vite
|
||||
|
||||
# Build workspaces
|
||||
npm run build:workspaces
|
||||
|
||||
# Build Storybook documentation
|
||||
npm run build-storybook
|
||||
```
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Start dev server (with live reload)
|
||||
npm run dev
|
||||
|
||||
# Start dev server connected to real backend
|
||||
npm run dev:server
|
||||
|
||||
# Start dev server with MSW mocks (default)
|
||||
npm run dev:mock
|
||||
|
||||
# Preview production build
|
||||
npm run preview
|
||||
```
|
||||
|
||||
### Test Commands
|
||||
|
||||
```bash
|
||||
# Run all unit tests
|
||||
npm test
|
||||
|
||||
# Run tests in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run tests in development mode
|
||||
npm run test:dev
|
||||
|
||||
# Run tests in watch mode (dev config)
|
||||
npm run test:dev-watch
|
||||
|
||||
# Run E2E tests with Playwright
|
||||
npm run test:e2e
|
||||
|
||||
# Run example tests
|
||||
npm run test:examples
|
||||
|
||||
# Run example tests in watch mode
|
||||
npm run test:examples:watch
|
||||
|
||||
# Run example tests in browser
|
||||
npm run test:examples:browser
|
||||
```
|
||||
|
||||
### Code Quality Commands
|
||||
|
||||
```bash
|
||||
# Lint TypeScript files
|
||||
npm run lint
|
||||
|
||||
# Lint and show only errors
|
||||
npm run lint:errors
|
||||
|
||||
# Lint and auto-fix issues
|
||||
npm run lint:fix
|
||||
|
||||
# Format code
|
||||
npm run format
|
||||
|
||||
# Format and auto-fix
|
||||
npm run format:fix
|
||||
|
||||
# Type check
|
||||
npm run compile
|
||||
|
||||
# Run all checks (lint, compile, build-storybook, jsonschema)
|
||||
npm run check
|
||||
```
|
||||
|
||||
### Code Generation Commands
|
||||
|
||||
```bash
|
||||
# Generate TypeScript config
|
||||
npm run generate:tsconfig
|
||||
|
||||
# Generate OpenAPI client from backend API
|
||||
npm run generate:server-api
|
||||
|
||||
# Generate icons
|
||||
npm run generate:icons
|
||||
|
||||
# Generate package manifest
|
||||
npm run generate:manifest
|
||||
|
||||
# Generate JSON schema for umbraco-package.json
|
||||
npm run generate:jsonschema
|
||||
|
||||
# Generate JSON schema to dist
|
||||
npm run generate:jsonschema:dist
|
||||
|
||||
# Generate UI API docs with TypeDoc
|
||||
npm run generate:ui-api-docs
|
||||
|
||||
# Generate const check tests
|
||||
npm run generate:check-const-test
|
||||
```
|
||||
|
||||
### Analysis Commands
|
||||
|
||||
```bash
|
||||
# Check for circular dependencies
|
||||
npm run check:circular
|
||||
|
||||
# Check module dependencies
|
||||
npm run check:module-dependencies
|
||||
|
||||
# Check path lengths
|
||||
npm run check:paths
|
||||
|
||||
# Analyze web components
|
||||
npm run wc-analyze
|
||||
|
||||
# Analyze web components for VS Code
|
||||
npm run wc-analyze:vscode
|
||||
```
|
||||
|
||||
### Storybook Commands
|
||||
|
||||
```bash
|
||||
# Start Storybook dev server
|
||||
npm run storybook
|
||||
|
||||
# Build Storybook
|
||||
npm run storybook:build
|
||||
|
||||
# Build and preview Storybook
|
||||
npm run storybook:preview
|
||||
```
|
||||
|
||||
### Package Management
|
||||
|
||||
```bash
|
||||
# Validate package exports
|
||||
npm run package:validate
|
||||
|
||||
# Prepare for npm publish
|
||||
npm run prepack
|
||||
```
|
||||
|
||||
### Environment Setup
|
||||
|
||||
**Prerequisites**:
|
||||
- Node.js >=22.17.1
|
||||
- npm >=10.9.2
|
||||
- Modern browser (Chrome, Firefox, Safari)
|
||||
|
||||
**Initial Setup**:
|
||||
|
||||
1. Clone repository
|
||||
```bash
|
||||
git clone https://github.com/umbraco/Umbraco-CMS.git
|
||||
cd Umbraco-CMS/src/Umbraco.Web.UI.Client
|
||||
```
|
||||
|
||||
2. Install dependencies
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
3. Configure environment (optional)
|
||||
```bash
|
||||
cp .env .env.local
|
||||
# Edit .env.local with your settings
|
||||
```
|
||||
|
||||
4. Start development
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
**Environment Variables** (see `.env` file):
|
||||
|
||||
- `VITE_UMBRACO_USE_MSW` - Enable/disable Mock Service Worker (`on`/`off`)
|
||||
- `VITE_UMBRACO_API_URL` - Backend API URL (e.g., `https://localhost:44339`)
|
||||
- `VITE_UMBRACO_INSTALL_STATUS` - Install status (`running`, `must-install`, `must-upgrade`)
|
||||
- `VITE_UMBRACO_EXTENSION_MOCKS` - Enable extension mocks (`on`/`off`)
|
||||
|
||||
584
src/Umbraco.Web.UI.Client/docs/edge-cases.md
Normal file
584
src/Umbraco.Web.UI.Client/docs/edge-cases.md
Normal file
@@ -0,0 +1,584 @@
|
||||
# Edge Cases
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Null/Undefined Handling
|
||||
|
||||
**Always Check**:
|
||||
|
||||
```typescript
|
||||
// Use optional chaining
|
||||
const name = this._content?.name;
|
||||
|
||||
// Use nullish coalescing for defaults
|
||||
const name = this._content?.name ?? 'Untitled';
|
||||
|
||||
// Check before accessing
|
||||
if (!this._content) {
|
||||
return html`<p>No content</p>`;
|
||||
}
|
||||
```
|
||||
|
||||
**TypeScript Strict Null Checks** (enabled):
|
||||
- Variables are non-nullable by default
|
||||
- Use `Type | undefined` or `Type | null` explicitly
|
||||
- TypeScript forces null checks
|
||||
|
||||
**Function Parameters**:
|
||||
|
||||
```typescript
|
||||
// Make nullable parameters explicit
|
||||
function load(id: string, culture?: string) {
|
||||
const cultureCode = culture ?? 'en-US'; // Default value
|
||||
}
|
||||
```
|
||||
|
||||
### Array Edge Cases
|
||||
|
||||
**Empty Arrays**:
|
||||
|
||||
```typescript
|
||||
// Check length before access
|
||||
if (this._items.length === 0) {
|
||||
return html`<p>No items</p>`;
|
||||
}
|
||||
|
||||
// Safe array methods
|
||||
const first = this._items[0]; // Could be undefined
|
||||
const first = this._items.at(0); // Also could be undefined
|
||||
|
||||
// Use optional chaining
|
||||
const firstId = this._items[0]?.id;
|
||||
```
|
||||
|
||||
**Single vs Multiple Items**:
|
||||
|
||||
```typescript
|
||||
// Handle both cases
|
||||
const items = Array.isArray(data) ? data : [data];
|
||||
```
|
||||
|
||||
**Array Methods on Undefined**:
|
||||
|
||||
```typescript
|
||||
// Guard against undefined
|
||||
const ids = this._items?.map(item => item.id) ?? [];
|
||||
|
||||
// Or check first
|
||||
if (!this._items) {
|
||||
return [];
|
||||
}
|
||||
return this._items.map(item => item.id);
|
||||
```
|
||||
|
||||
**Sparse Arrays** (rare in this codebase):
|
||||
|
||||
```typescript
|
||||
// Use filter to remove empty slots
|
||||
const dense = sparse.filter(() => true);
|
||||
```
|
||||
|
||||
### String Edge Cases
|
||||
|
||||
**Empty Strings**:
|
||||
|
||||
```typescript
|
||||
// Check for empty strings
|
||||
if (!name || name.trim().length === 0) {
|
||||
return 'Untitled';
|
||||
}
|
||||
|
||||
// Or use default
|
||||
const displayName = name?.trim() || 'Untitled';
|
||||
```
|
||||
|
||||
**String vs Null/Undefined**:
|
||||
|
||||
```typescript
|
||||
// Distinguish between empty string and null
|
||||
const hasValue = value !== null && value !== undefined;
|
||||
const isEmpty = value === '';
|
||||
|
||||
// Or use optional chaining
|
||||
const length = value?.length ?? 0;
|
||||
```
|
||||
|
||||
**Trim Whitespace**:
|
||||
|
||||
```typescript
|
||||
// Always trim user input
|
||||
const cleanName = this._name.trim();
|
||||
|
||||
// Validate after trimming
|
||||
if (cleanName.length === 0) {
|
||||
// Invalid
|
||||
}
|
||||
```
|
||||
|
||||
**String Encoding**:
|
||||
- Use UTF-8 everywhere
|
||||
- Be aware of Unicode characters (emojis, etc.)
|
||||
- Use `textContent` not `innerHTML` for plain text
|
||||
|
||||
**Internationalization**:
|
||||
|
||||
```typescript
|
||||
// Use localization API
|
||||
const label = this.localize.term('general_submit');
|
||||
|
||||
// Not hardcoded strings
|
||||
// const label = 'Submit'; // ❌
|
||||
```
|
||||
|
||||
### Number Edge Cases
|
||||
|
||||
**NaN Checks**:
|
||||
|
||||
```typescript
|
||||
// Use Number.isNaN, not isNaN
|
||||
if (Number.isNaN(value)) {
|
||||
// Handle NaN
|
||||
}
|
||||
|
||||
// isNaN coerces, Number.isNaN doesn't
|
||||
isNaN('hello'); // true (coerces to NaN)
|
||||
Number.isNaN('hello'); // false (not a number)
|
||||
```
|
||||
|
||||
**Infinity**:
|
||||
|
||||
```typescript
|
||||
if (!Number.isFinite(value)) {
|
||||
// Handle Infinity or NaN
|
||||
}
|
||||
```
|
||||
|
||||
**Parsing**:
|
||||
|
||||
```typescript
|
||||
// parseInt/parseFloat can return NaN
|
||||
const num = parseInt(input, 10);
|
||||
if (Number.isNaN(num)) {
|
||||
// Handle invalid input
|
||||
}
|
||||
|
||||
// Or use Number constructor with validation
|
||||
const num = Number(input);
|
||||
if (!Number.isFinite(num)) {
|
||||
// Invalid
|
||||
}
|
||||
```
|
||||
|
||||
**Floating Point Precision**:
|
||||
|
||||
```typescript
|
||||
// Don't compare floats with ===
|
||||
const isEqual = Math.abs(a - b) < 0.0001;
|
||||
|
||||
// Or use integers for currency (cents, not dollars)
|
||||
const priceInCents = 1099; // $10.99
|
||||
```
|
||||
|
||||
**Division by Zero**:
|
||||
|
||||
```typescript
|
||||
// JavaScript returns Infinity, not error
|
||||
const result = 10 / 0; // Infinity
|
||||
|
||||
// Check denominator
|
||||
if (denominator === 0) {
|
||||
// Handle division by zero
|
||||
return 0; // Or throw error
|
||||
}
|
||||
```
|
||||
|
||||
**Safe Integer Range**:
|
||||
|
||||
```typescript
|
||||
// JavaScript integers are safe up to Number.MAX_SAFE_INTEGER
|
||||
const isSafe = Number.isSafeInteger(value);
|
||||
|
||||
// For IDs, use strings not numbers
|
||||
interface UmbContentModel {
|
||||
id: string; // Not number
|
||||
}
|
||||
```
|
||||
|
||||
### Object Edge Cases
|
||||
|
||||
**Property Existence**:
|
||||
|
||||
```typescript
|
||||
// Use optional chaining
|
||||
const value = obj?.property;
|
||||
|
||||
// Or check explicitly
|
||||
if ('property' in obj) {
|
||||
const value = obj.property;
|
||||
}
|
||||
|
||||
// hasOwnProperty (not inherited)
|
||||
if (Object.hasOwn(obj, 'property')) {
|
||||
// Property exists on object itself
|
||||
}
|
||||
```
|
||||
|
||||
**Null vs Undefined vs {}**:
|
||||
|
||||
```typescript
|
||||
// Distinguish between missing and empty
|
||||
const isEmpty = obj !== null && obj !== undefined && Object.keys(obj).length === 0;
|
||||
|
||||
// Or use optional chaining
|
||||
const hasData = obj && Object.keys(obj).length > 0;
|
||||
```
|
||||
|
||||
**Shallow vs Deep Copy**:
|
||||
|
||||
```typescript
|
||||
// Shallow copy
|
||||
const copy = { ...original };
|
||||
|
||||
// Deep copy (for simple objects)
|
||||
const deepCopy = JSON.parse(JSON.stringify(original));
|
||||
|
||||
// Deep copy with structuredClone (modern browsers)
|
||||
const deepCopy = structuredClone(original);
|
||||
|
||||
// Note: Functions, symbols, and undefined are not copied by JSON.stringify
|
||||
```
|
||||
|
||||
**Object Freezing**:
|
||||
|
||||
```typescript
|
||||
// Prevent modification
|
||||
const frozen = Object.freeze(obj);
|
||||
|
||||
// Check if frozen
|
||||
if (Object.isFrozen(obj)) {
|
||||
// Can't modify
|
||||
}
|
||||
```
|
||||
|
||||
### Async/Await Edge Cases
|
||||
|
||||
**Unhandled Promise Rejections**:
|
||||
|
||||
```typescript
|
||||
// Always catch errors
|
||||
try {
|
||||
await asyncOperation();
|
||||
} catch (error) {
|
||||
console.error('Failed:', error);
|
||||
}
|
||||
|
||||
// Or use .catch()
|
||||
asyncOperation().catch(error => {
|
||||
console.error('Failed:', error);
|
||||
});
|
||||
|
||||
// For fire-and-forget, explicitly catch
|
||||
void asyncOperation().catch(error => console.error(error));
|
||||
```
|
||||
|
||||
**Promise.all Fails Fast**:
|
||||
|
||||
```typescript
|
||||
// Promise.all rejects if ANY promise rejects
|
||||
try {
|
||||
const results = await Promise.all([op1(), op2(), op3()]);
|
||||
} catch (error) {
|
||||
// One failed, others may still be running
|
||||
}
|
||||
|
||||
// Use Promise.allSettled to wait for all (even if some fail)
|
||||
const results = await Promise.allSettled([op1(), op2(), op3()]);
|
||||
results.forEach((result, index) => {
|
||||
if (result.status === 'fulfilled') {
|
||||
console.log(`Op ${index} succeeded:`, result.value);
|
||||
} else {
|
||||
console.error(`Op ${index} failed:`, result.reason);
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
**Race Conditions**:
|
||||
|
||||
```typescript
|
||||
// Avoid race conditions with sequential operations
|
||||
this._loading = true;
|
||||
try {
|
||||
const data1 = await fetchData1();
|
||||
const data2 = await fetchData2(data1.id);
|
||||
this._result = processData(data1, data2);
|
||||
} finally {
|
||||
this._loading = false;
|
||||
}
|
||||
|
||||
// For parallel operations, use Promise.all
|
||||
const [data1, data2] = await Promise.all([fetchData1(), fetchData2()]);
|
||||
```
|
||||
|
||||
**Timeout Handling**:
|
||||
|
||||
```typescript
|
||||
// Implement timeout for operations
|
||||
function withTimeout<T>(promise: Promise<T>, ms: number): Promise<T> {
|
||||
return Promise.race([
|
||||
promise,
|
||||
new Promise<T>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), ms)
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
// Usage
|
||||
try {
|
||||
const data = await withTimeout(fetchData(), 5000);
|
||||
} catch (error) {
|
||||
// Handle timeout or other errors
|
||||
}
|
||||
```
|
||||
|
||||
**Memory Leaks with Event Listeners**:
|
||||
|
||||
```typescript
|
||||
export class UmbMyElement extends LitElement {
|
||||
#controller = new UmbMyController(this);
|
||||
|
||||
// Controllers automatically clean up on disconnect
|
||||
constructor() {
|
||||
super();
|
||||
this.#controller.observe(dataSource$, (value) => {
|
||||
this._data = value;
|
||||
});
|
||||
}
|
||||
|
||||
// Lit lifecycle handles this automatically
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
// Controllers are destroyed automatically
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Web Component Edge Cases
|
||||
|
||||
**Custom Element Not Defined**:
|
||||
|
||||
```typescript
|
||||
// Check if element is defined
|
||||
if (!customElements.get('umb-my-element')) {
|
||||
// Not defined yet
|
||||
await customElements.whenDefined('umb-my-element');
|
||||
}
|
||||
|
||||
// Or use upgrade
|
||||
await customElements.upgrade(element);
|
||||
```
|
||||
|
||||
**Shadow DOM Queries**:
|
||||
|
||||
```typescript
|
||||
// Query shadow root, not document
|
||||
const button = this.shadowRoot?.querySelector('button');
|
||||
|
||||
// Use Lit decorators
|
||||
@query('#myButton')
|
||||
private _button!: HTMLButtonElement;
|
||||
```
|
||||
|
||||
**Property vs Attribute Sync**:
|
||||
|
||||
```typescript
|
||||
// Lit keeps properties and attributes in sync
|
||||
@property({ type: String })
|
||||
name = ''; // Syncs with name="" attribute
|
||||
|
||||
// But complex types don't sync to attributes
|
||||
@property({ type: Object })
|
||||
data = {}; // No attribute sync
|
||||
|
||||
// State doesn't sync to attributes
|
||||
@state()
|
||||
private _loading = false;
|
||||
```
|
||||
|
||||
**Reactive Update Timing**:
|
||||
|
||||
```typescript
|
||||
// Wait for update to complete
|
||||
this.value = 'new value';
|
||||
await this.updateComplete;
|
||||
// Now DOM is updated
|
||||
|
||||
// Or use requestUpdate
|
||||
this.requestUpdate();
|
||||
await this.updateComplete;
|
||||
```
|
||||
|
||||
### Date/Time Edge Cases
|
||||
|
||||
**Use Luxon** (not Date):
|
||||
|
||||
```typescript
|
||||
import { DateTime } from '@umbraco-cms/backoffice/external/luxon';
|
||||
|
||||
// Create dates
|
||||
const now = DateTime.now();
|
||||
const utc = DateTime.utc();
|
||||
const parsed = DateTime.fromISO('2024-01-15T10:30:00Z');
|
||||
|
||||
// Always store dates in UTC
|
||||
const isoString = now.toUTC().toISO();
|
||||
|
||||
// Format for display
|
||||
const formatted = now.toLocaleString(DateTime.DATETIME_MED);
|
||||
|
||||
// Timezone handling
|
||||
const local = utc.setZone('local');
|
||||
```
|
||||
|
||||
**Date Comparison**:
|
||||
|
||||
```typescript
|
||||
// Compare DateTime objects
|
||||
if (date1 < date2) { }
|
||||
if (date1.equals(date2)) { }
|
||||
|
||||
// Or compare timestamps
|
||||
if (date1.toMillis() < date2.toMillis()) { }
|
||||
```
|
||||
|
||||
**Date Parsing Can Fail**:
|
||||
|
||||
```typescript
|
||||
const date = DateTime.fromISO(input);
|
||||
if (!date.isValid) {
|
||||
console.error('Invalid date:', date.invalidReason);
|
||||
// Handle invalid date
|
||||
}
|
||||
```
|
||||
|
||||
### JSON Edge Cases
|
||||
|
||||
**JSON.parse Can Throw**:
|
||||
|
||||
```typescript
|
||||
// Always wrap in try/catch
|
||||
try {
|
||||
const data = JSON.parse(jsonString);
|
||||
} catch (error) {
|
||||
console.error('Invalid JSON:', error);
|
||||
// Handle parse error
|
||||
}
|
||||
```
|
||||
|
||||
**Circular References**:
|
||||
|
||||
```typescript
|
||||
// JSON.stringify throws on circular references
|
||||
const obj = { a: 1 };
|
||||
obj.self = obj;
|
||||
|
||||
try {
|
||||
JSON.stringify(obj);
|
||||
} catch (error) {
|
||||
// TypeError: Converting circular structure to JSON
|
||||
}
|
||||
```
|
||||
|
||||
**Date Objects**:
|
||||
|
||||
```typescript
|
||||
// Dates become strings
|
||||
const data = { date: new Date() };
|
||||
const json = JSON.stringify(data);
|
||||
const parsed = JSON.parse(json);
|
||||
// parsed.date is a string, not Date
|
||||
|
||||
// Use ISO format explicitly
|
||||
const isoDate = new Date().toISOString();
|
||||
```
|
||||
|
||||
**Undefined Values**:
|
||||
|
||||
```typescript
|
||||
// Undefined values are omitted
|
||||
const obj = { a: 1, b: undefined };
|
||||
JSON.stringify(obj); // '{"a":1}'
|
||||
|
||||
// Use null for explicit absence
|
||||
const obj = { a: 1, b: null };
|
||||
JSON.stringify(obj); // '{"a":1,"b":null}'
|
||||
```
|
||||
|
||||
### Handling Strategy
|
||||
|
||||
**Guard Clauses**:
|
||||
|
||||
```typescript
|
||||
// Check preconditions early
|
||||
if (!this._data) return;
|
||||
if (this._data.length === 0) return;
|
||||
if (!this._data[0].name) return;
|
||||
|
||||
// Now can safely use data
|
||||
this.processData(this._data[0].name);
|
||||
```
|
||||
|
||||
**Defensive Programming**:
|
||||
|
||||
```typescript
|
||||
// Validate inputs
|
||||
function process(items: unknown) {
|
||||
if (!Array.isArray(items)) {
|
||||
throw new Error('Expected array');
|
||||
}
|
||||
// Safe to use items as array
|
||||
}
|
||||
|
||||
// Use type guards
|
||||
if (isContentModel(data)) {
|
||||
// TypeScript knows data is UmbContentModel
|
||||
}
|
||||
```
|
||||
|
||||
**Fail Fast**:
|
||||
|
||||
```typescript
|
||||
// Throw errors early for programmer mistakes
|
||||
if (this._repository === undefined) {
|
||||
throw new Error('Repository not initialized');
|
||||
}
|
||||
|
||||
// Handle expected errors gracefully
|
||||
try {
|
||||
const data = await this._repository.loadById(id);
|
||||
} catch (error) {
|
||||
this._error = 'Failed to load content';
|
||||
return;
|
||||
}
|
||||
```
|
||||
|
||||
**Document Edge Cases**:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Loads content by ID.
|
||||
* @throws {UmbContentNotFoundError} If content doesn't exist
|
||||
* @throws {UmbUnauthorizedError} If user lacks permission
|
||||
* @returns {Promise<UmbContentModel>} The content model
|
||||
*
|
||||
* @remarks
|
||||
* This method returns cached data if available.
|
||||
* Pass `{ skipCache: true }` to force a fresh load.
|
||||
*/
|
||||
async loadById(id: string, options?: LoadOptions): Promise<UmbContentModel> {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
242
src/Umbraco.Web.UI.Client/docs/error-handling.md
Normal file
242
src/Umbraco.Web.UI.Client/docs/error-handling.md
Normal file
@@ -0,0 +1,242 @@
|
||||
# Error Handling
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Diagnosis Process
|
||||
|
||||
1. **Check browser console** for errors and warnings
|
||||
2. **Reproduce consistently** - Identify exact steps
|
||||
3. **Check network tab** for failed API calls
|
||||
4. **Inspect element** to verify DOM structure
|
||||
5. **Check Lit reactive update cycle** - Use `element.updateComplete`
|
||||
6. **Verify context availability** - Check context providers
|
||||
7. **Review event listeners** - Check event propagation
|
||||
8. **Use browser debugger** with source maps
|
||||
|
||||
**Common Web Component Issues**:
|
||||
- Component not rendering: Check if custom element is defined
|
||||
- Properties not updating: Verify `@property()` decorator and reactive update cycle
|
||||
- Events not firing: Check event names and listeners
|
||||
- Shadow DOM issues: Use `shadowRoot.querySelector()` not `querySelector()`
|
||||
- Styles not applied: Check Shadow DOM style encapsulation
|
||||
- Context not available: Ensure context provider is ancestor in DOM tree
|
||||
|
||||
### Error Handling Standards
|
||||
|
||||
**Error Classes**:
|
||||
|
||||
```typescript
|
||||
// Use built-in Error class or extend it
|
||||
throw new Error('Failed to load content');
|
||||
|
||||
// Custom error classes for domain errors
|
||||
export class UmbContentNotFoundError extends Error {
|
||||
constructor(id: string) {
|
||||
super(`Content with id "${id}" not found`);
|
||||
this.name = 'UmbContentNotFoundError';
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Repository Pattern Error Handling**:
|
||||
|
||||
```typescript
|
||||
async requestById(id: string): Promise<{ data?: UmbContentModel; error?: Error }> {
|
||||
try {
|
||||
const response = await this._apiClient.getById({ id });
|
||||
return { data: response.data };
|
||||
} catch (error) {
|
||||
return { error: error as Error };
|
||||
}
|
||||
}
|
||||
|
||||
// Usage
|
||||
const { data, error } = await repository.requestById('123');
|
||||
if (error) {
|
||||
// Handle error
|
||||
console.error('Failed to load content:', error);
|
||||
return;
|
||||
}
|
||||
// Use data
|
||||
```
|
||||
|
||||
**Observable Error Handling**:
|
||||
|
||||
```typescript
|
||||
this.observe(dataSource$, (value) => {
|
||||
// Success handler
|
||||
this._data = value;
|
||||
}).catch((error) => {
|
||||
// Error handler
|
||||
console.error('Observable error:', error);
|
||||
});
|
||||
```
|
||||
|
||||
**Promise Error Handling**:
|
||||
|
||||
```typescript
|
||||
// Always use try/catch with async/await
|
||||
async myMethod() {
|
||||
try {
|
||||
const result = await this.fetchData();
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch data:', error);
|
||||
throw error; // Re-throw or handle
|
||||
}
|
||||
}
|
||||
|
||||
// Or use .catch()
|
||||
this.fetchData()
|
||||
.then(result => this.handleResult(result))
|
||||
.catch(error => this.handleError(error));
|
||||
```
|
||||
|
||||
### Web Component Error Handling
|
||||
|
||||
**Lifecycle Errors**:
|
||||
|
||||
```typescript
|
||||
export class UmbMyElement extends UmbElementMixin(LitElement) {
|
||||
constructor() {
|
||||
try {
|
||||
super();
|
||||
// Initialization that might throw
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize element:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async connectedCallback() {
|
||||
try {
|
||||
super.connectedCallback();
|
||||
// Async initialization
|
||||
await this.loadData();
|
||||
} catch (error) {
|
||||
console.error('Failed to connect element:', error);
|
||||
this._errorMessage = 'Failed to load component';
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Render Errors**:
|
||||
|
||||
```typescript
|
||||
override render() {
|
||||
if (this._error) {
|
||||
return html`<umb-error-message .error=${this._error}></umb-error-message>`;
|
||||
}
|
||||
|
||||
if (!this._data) {
|
||||
return html`<uui-loader></uui-loader>`;
|
||||
}
|
||||
|
||||
return html`
|
||||
<!-- Normal render -->
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
### Logging Standards
|
||||
|
||||
**Console Logging**:
|
||||
|
||||
```typescript
|
||||
// Development only - Remove before production
|
||||
console.log('Debug info:', data);
|
||||
|
||||
// Errors - Kept in production but sanitized
|
||||
console.error('Failed to load:', error);
|
||||
|
||||
// Warnings
|
||||
console.warn('Deprecated API usage:', method);
|
||||
|
||||
// Avoid console.log in production code (ESLint warning)
|
||||
```
|
||||
|
||||
**Custom Logging** (if needed):
|
||||
|
||||
```typescript
|
||||
// Use debug flag for verbose logging
|
||||
if (this._debug) {
|
||||
console.log('[UmbMyComponent]', 'State changed:', this._state);
|
||||
}
|
||||
```
|
||||
|
||||
**Don't Log Sensitive Data**:
|
||||
- User credentials
|
||||
- API tokens
|
||||
- Personal information (PII)
|
||||
- Session IDs
|
||||
- Full error stack traces in production
|
||||
|
||||
### Development Environment
|
||||
|
||||
- **Source Maps**: Enabled in Vite config for debugging
|
||||
- **Error Overlay**: Vite provides error overlay in development
|
||||
- **Hot Module Replacement**: Instant feedback on code changes
|
||||
- **Detailed Errors**: Full stack traces with source locations
|
||||
- **TypeScript Checking**: Real-time type checking in IDE
|
||||
|
||||
### Production Environment
|
||||
|
||||
- **Sanitized Errors**: No stack traces exposed to users
|
||||
- **User-Friendly Messages**: Show helpful error messages
|
||||
- **Error Boundaries**: Catch errors at component boundaries
|
||||
- **Graceful Degradation**: Fallback UI when errors occur
|
||||
- **Error Reporting**: Log errors to console (browser dev tools)
|
||||
|
||||
**Production Error Display**:
|
||||
|
||||
```typescript
|
||||
private _errorMessage?: string;
|
||||
|
||||
override render() {
|
||||
if (this._errorMessage) {
|
||||
return html`
|
||||
<uui-box>
|
||||
<p class="error">${this._errorMessage}</p>
|
||||
<uui-button @click=${this._retry} label="Try Again"></uui-button>
|
||||
</uui-box>
|
||||
`;
|
||||
}
|
||||
// ... normal render
|
||||
}
|
||||
```
|
||||
|
||||
### Context-Specific Error Handling
|
||||
|
||||
**HTTP Client Errors** (OpenAPI):
|
||||
|
||||
```typescript
|
||||
try {
|
||||
const response = await this._apiClient.getDocument({ id });
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
if (error.status === 404) {
|
||||
throw new UmbContentNotFoundError(id);
|
||||
}
|
||||
if (error.status === 403) {
|
||||
throw new UmbUnauthorizedError('Access denied');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
```
|
||||
|
||||
**Observable Subscription Errors**:
|
||||
|
||||
```typescript
|
||||
this._subscription = this._dataSource.asObservable().subscribe({
|
||||
next: (value) => this._data = value,
|
||||
error: (error) => {
|
||||
console.error('Observable error:', error);
|
||||
this._errorMessage = 'Failed to load data';
|
||||
},
|
||||
complete: () => console.log('Observable completed'),
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
299
src/Umbraco.Web.UI.Client/docs/security.md
Normal file
299
src/Umbraco.Web.UI.Client/docs/security.md
Normal file
@@ -0,0 +1,299 @@
|
||||
# Security
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Input Validation
|
||||
|
||||
**Validate All User Input**:
|
||||
|
||||
```typescript
|
||||
// Use validation in forms
|
||||
import { UmbValidationController } from '@umbraco-cms/backoffice/validation';
|
||||
|
||||
#validation = new UmbValidationController(this);
|
||||
|
||||
async #handleSubmit() {
|
||||
if (!this.#validation.validate()) {
|
||||
return; // Show validation errors
|
||||
}
|
||||
// Proceed with submission
|
||||
}
|
||||
```
|
||||
|
||||
**String Validation**:
|
||||
|
||||
```typescript
|
||||
private _validateName(name: string): boolean {
|
||||
// Length check
|
||||
if (name.length === 0 || name.length > 100) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Pattern check (example: alphanumeric and spaces)
|
||||
if (!/^[a-zA-Z0-9\s]+$/.test(name)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
**Sanitize HTML**:
|
||||
|
||||
```typescript
|
||||
// Use DOMPurify for HTML sanitization
|
||||
import DOMPurify from '@umbraco-cms/backoffice/external/dompurify';
|
||||
|
||||
const cleanHtml = DOMPurify.sanitize(userInput);
|
||||
|
||||
// In Lit templates, use unsafeHTML directive with sanitized content
|
||||
import { unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
|
||||
|
||||
render() {
|
||||
return html`<div>${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}</div>`;
|
||||
}
|
||||
```
|
||||
|
||||
### Authentication & Authorization
|
||||
|
||||
**OpenID Connect** via backend:
|
||||
- Backoffice uses OpenID Connect for authentication
|
||||
- Authentication handled by .NET backend
|
||||
- Tokens managed by browser (httpOnly cookies)
|
||||
|
||||
**Authorization Checks**:
|
||||
|
||||
```typescript
|
||||
// Check user permissions before actions
|
||||
#authContext?: UmbAuthContext;
|
||||
|
||||
async #handleDelete() {
|
||||
const hasPermission = await this.#authContext?.hasPermission('delete');
|
||||
if (!hasPermission) {
|
||||
// Show error or hide action
|
||||
return;
|
||||
}
|
||||
// Proceed with deletion
|
||||
}
|
||||
```
|
||||
|
||||
**Context Security**:
|
||||
- Use Context API for auth state
|
||||
- Don't store sensitive tokens in localStorage
|
||||
- Backend handles token refresh
|
||||
|
||||
### API Security
|
||||
|
||||
**HTTP Client Security**:
|
||||
|
||||
```typescript
|
||||
// OpenAPI client handles:
|
||||
// - CSRF tokens
|
||||
// - Request headers
|
||||
// - Credentials
|
||||
// - Error handling
|
||||
|
||||
// Use generated OpenAPI client
|
||||
import { ContentResource } from '@umbraco-cms/backoffice/external/backend-api';
|
||||
|
||||
const client = new ContentResource();
|
||||
const response = await client.getById({ id });
|
||||
```
|
||||
|
||||
**CORS** (Backend Configuration):
|
||||
- Configured in .NET backend
|
||||
- Backoffice follows same-origin policy
|
||||
- API calls to same origin
|
||||
|
||||
**Rate Limiting** (Backend):
|
||||
- Handled by .NET backend
|
||||
- Backoffice respects rate limit headers
|
||||
|
||||
### XSS Prevention
|
||||
|
||||
**Template Security** (Lit):
|
||||
|
||||
```typescript
|
||||
// Lit automatically escapes content in templates
|
||||
render() {
|
||||
// Safe - Automatically escaped
|
||||
return html`<div>${this.userContent}</div>`;
|
||||
|
||||
// UNSAFE - Only use with sanitized content
|
||||
return html`<div>${unsafeHTML(DOMPurify.sanitize(this.htmlContent))}</div>`;
|
||||
}
|
||||
```
|
||||
|
||||
**Attribute Binding**:
|
||||
|
||||
```typescript
|
||||
// Safe - Lit escapes attribute values
|
||||
render() {
|
||||
return html`<input value=${this.userInput} />`;
|
||||
}
|
||||
```
|
||||
|
||||
**Event Handlers**:
|
||||
|
||||
```typescript
|
||||
// Safe - Event handlers are not strings
|
||||
render() {
|
||||
return html`<button @click=${this.#handleClick}>Click</button>`;
|
||||
}
|
||||
|
||||
// NEVER do this (code injection risk)
|
||||
// render() {
|
||||
// return html`<button onclick="${this.userCode}">Click</button>`;
|
||||
// }
|
||||
```
|
||||
|
||||
### Content Security Policy
|
||||
|
||||
**CSP Headers** (Backend Configuration):
|
||||
- Configured in .NET backend
|
||||
- Restricts script sources
|
||||
- Prevents inline scripts (except with nonce)
|
||||
- Reports violations
|
||||
|
||||
**Backoffice Compliance**:
|
||||
- No inline scripts
|
||||
- No `eval()` or `Function()` constructor
|
||||
- Monaco Editor uses web workers (CSP compliant)
|
||||
|
||||
### Dependencies Security
|
||||
|
||||
**Package Management**:
|
||||
|
||||
```bash
|
||||
# Check for vulnerabilities
|
||||
npm audit
|
||||
|
||||
# Fix automatically
|
||||
npm audit fix
|
||||
|
||||
# Update dependencies carefully
|
||||
npm update
|
||||
```
|
||||
|
||||
**Dependency Security Practices**:
|
||||
- Renovate bot automatically creates PRs for updates
|
||||
- Review dependency changes before merging
|
||||
- Only use packages from npm registry
|
||||
- Verify package integrity
|
||||
- Keep dependencies updated
|
||||
|
||||
**Known Vulnerabilities**:
|
||||
- CI checks for vulnerabilities on every PR
|
||||
- Security advisories reviewed regularly
|
||||
|
||||
### Common Vulnerabilities
|
||||
|
||||
**XSS (Cross-Site Scripting)**:
|
||||
- ✅ Lit templates automatically escape content
|
||||
- ✅ DOMPurify for HTML sanitization
|
||||
- ❌ Never use `unsafeHTML` with user input directly
|
||||
- ❌ Never set `innerHTML` with user input
|
||||
|
||||
**CSRF (Cross-Site Request Forgery)**:
|
||||
- ✅ Backend sends CSRF tokens
|
||||
- ✅ OpenAPI client includes tokens automatically
|
||||
- ✅ SameSite cookies
|
||||
|
||||
**Injection Attacks**:
|
||||
- ✅ Backend uses parameterized queries
|
||||
- ✅ Input validation on both frontend and backend
|
||||
- ✅ OpenAPI client prevents injection
|
||||
|
||||
**Prototype Pollution**:
|
||||
- ❌ Never use `Object.assign` with user input as source
|
||||
- ❌ Never use `_.merge` with untrusted data
|
||||
- ✅ Validate object shapes before using
|
||||
|
||||
**ReDoS (Regular Expression Denial of Service)**:
|
||||
- ✅ Review complex regex patterns
|
||||
- ✅ Test regex with long inputs
|
||||
- ❌ Avoid backtracking in regex
|
||||
|
||||
### Secure Coding Practices
|
||||
|
||||
**Don't Trust Client Data**:
|
||||
- Validate on backend (primary defense)
|
||||
- Frontend validation is UX, not security
|
||||
|
||||
**Principle of Least Privilege**:
|
||||
- Only request permissions needed
|
||||
- Check permissions before sensitive operations
|
||||
- Hide UI for unavailable actions
|
||||
|
||||
**Sanitize Output**:
|
||||
- Always sanitize HTML before rendering
|
||||
- Escape special characters in user content
|
||||
- Use Lit's automatic escaping
|
||||
|
||||
**Secure Defaults**:
|
||||
- Forms should validate by default
|
||||
- Sensitive operations require confirmation
|
||||
- Errors don't expose sensitive information
|
||||
|
||||
**Defense in Depth**:
|
||||
- Multiple layers of security
|
||||
- Frontend validation + Backend validation
|
||||
- Input sanitization + Output escaping
|
||||
- Authentication + Authorization
|
||||
|
||||
### Security Anti-Patterns to Avoid
|
||||
|
||||
❌ **Never do this**:
|
||||
```typescript
|
||||
// XSS vulnerability
|
||||
element.innerHTML = userInput;
|
||||
|
||||
// Code injection
|
||||
eval(userCode);
|
||||
|
||||
// Exposing sensitive data
|
||||
console.log('Token:', authToken);
|
||||
|
||||
// Storing secrets
|
||||
localStorage.setItem('apiKey', key);
|
||||
|
||||
// Disabling validation
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line
|
||||
|
||||
// Trusting user input
|
||||
const url = userInput; // Could be javascript:alert()
|
||||
window.location.href = url;
|
||||
```
|
||||
|
||||
✅ **Do this instead**:
|
||||
```typescript
|
||||
// Safe HTML rendering
|
||||
element.textContent = userInput;
|
||||
// or
|
||||
render() {
|
||||
return html`<div>${userInput}</div>`;
|
||||
}
|
||||
|
||||
// No eval needed
|
||||
// Use proper JavaScript patterns
|
||||
|
||||
// Don't log sensitive data
|
||||
console.log('Operation completed');
|
||||
|
||||
// Backend manages secrets
|
||||
// Frontend receives tokens via httpOnly cookies
|
||||
|
||||
// Fix TypeScript/ESLint issues properly
|
||||
// Don't suppress warnings
|
||||
|
||||
// Validate and sanitize URLs
|
||||
const url = new URL(userInput, window.location.origin);
|
||||
if (url.protocol === 'https:' || url.protocol === 'http:') {
|
||||
window.location.href = url.href;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
157
src/Umbraco.Web.UI.Client/docs/style-guide.md
Normal file
157
src/Umbraco.Web.UI.Client/docs/style-guide.md
Normal file
@@ -0,0 +1,157 @@
|
||||
# Style Guide
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
**Files**:
|
||||
- Web components: `my-component.element.ts`
|
||||
- Tests: `my-component.test.ts`
|
||||
- Stories: `my-component.stories.ts`
|
||||
- Controllers: `my-component.controller.ts`
|
||||
- Contexts: `my-component.context.ts`
|
||||
- Modals: `my-component.modal.ts`
|
||||
- Workspaces: `my-component.workspace.ts`
|
||||
- Repositories: `my-component.repository.ts`
|
||||
- Index files: `index.ts` (barrel exports)
|
||||
|
||||
**Classes & Types**:
|
||||
- Classes: `PascalCase` with `Umb` prefix: `UmbMyComponent`
|
||||
- Interfaces: `PascalCase` with `Umb` prefix: `UmbMyInterface`
|
||||
- Types: `PascalCase` with `Umb`, `Ufm`, `Manifest`, `Meta`, or `Example` prefix
|
||||
- Exported types MUST have approved prefix
|
||||
- Example types for docs: `ExampleMyType`
|
||||
|
||||
**Variables & Functions**:
|
||||
- Public members: `camelCase` without underscore: `myVariable`, `myMethod`
|
||||
- Private members: `camelCase` with leading underscore: `_myPrivateVariable`
|
||||
- #private members: `camelCase` without underscore: `#myPrivateField`
|
||||
- Protected members: `camelCase` with optional underscore: `myProtected` or `_myProtected`
|
||||
- Constants (exported): `UPPER_SNAKE_CASE` with `UMB_` prefix: `UMB_MY_CONSTANT`
|
||||
- Local constants: `UPPER_CASE` or `camelCase`
|
||||
|
||||
**Custom Elements**:
|
||||
- Element tag names: kebab-case with `umb-` prefix: `umb-my-component`
|
||||
- Must be registered in global `HTMLElementTagNameMap`
|
||||
|
||||
### File Organization
|
||||
|
||||
- One class/component per file
|
||||
- Use barrel exports (`index.ts`) for package public APIs
|
||||
- Import order (enforced by ESLint):
|
||||
1. External dependencies
|
||||
2. Parent imports
|
||||
3. Sibling imports
|
||||
4. Index imports
|
||||
5. Type-only imports (separate)
|
||||
|
||||
### Code Formatting (Prettier)
|
||||
|
||||
```json
|
||||
{
|
||||
"printWidth": 120,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": true,
|
||||
"useTabs": true
|
||||
}
|
||||
```
|
||||
|
||||
- **Indentation**: Tabs (not spaces)
|
||||
- **Line length**: 120 characters max
|
||||
- **Quotes**: Single quotes
|
||||
- **Semicolons**: Required
|
||||
- **Trailing commas**: Yes (default)
|
||||
|
||||
### TypeScript Conventions
|
||||
|
||||
**Strict Mode** (enabled in `tsconfig.json`):
|
||||
- `strict: true`
|
||||
- `noImplicitReturns: true`
|
||||
- `noFallthroughCasesInSwitch: true`
|
||||
- `noImplicitOverride: true`
|
||||
|
||||
**Type Features**:
|
||||
- Use TypeScript types over JSDoc when possible
|
||||
- BUT: Lit components use JSDoc for web-component-analyzer compatibility
|
||||
- Use `type` for unions/intersections: `type MyType = A | B`
|
||||
- Use `interface` for object shapes and extension: `interface MyInterface extends Base`
|
||||
- Prefer `const` over `let`, never use `var`
|
||||
- Use `readonly` for immutable properties
|
||||
- Use generics for reusable code
|
||||
- Avoid `any` (lint warning), use `unknown` instead
|
||||
- Use type guards for narrowing
|
||||
- Use `as const` for literal types
|
||||
|
||||
**Module Syntax**:
|
||||
- ES Modules only: `import`/`export`
|
||||
- Use consistent type imports: `import type { MyType } from '...'`
|
||||
- Use consistent type exports: `export type { MyType }`
|
||||
- No side-effects in imports
|
||||
|
||||
**Decorators**:
|
||||
- `@customElement('umb-my-element')` - Register custom element
|
||||
- `@property({ type: String })` - Reactive properties
|
||||
- `@state()` - Internal reactive state
|
||||
- `@query('#myId')` - Query shadow DOM
|
||||
- Experimental decorators enabled
|
||||
|
||||
### Modern TypeScript Features to Use
|
||||
|
||||
- **Async/await** over callbacks
|
||||
- **Optional chaining**: `obj?.property?.method?.()`
|
||||
- **Nullish coalescing**: `value ?? defaultValue`
|
||||
- **Template literals**: `` `Hello ${name}` ``
|
||||
- **Destructuring**: `const { a, b } = obj`
|
||||
- **Spread operator**: `{ ...obj, newProp: value }`
|
||||
- **Arrow functions**: `const fn = () => {}`
|
||||
- **Array methods**: `map`, `filter`, `reduce`, `find`, `some`, `every`
|
||||
- **Object methods**: `Object.keys`, `Object.values`, `Object.entries`
|
||||
- **Private fields**: `#privateField`
|
||||
|
||||
### Language Features to Avoid
|
||||
|
||||
- `var` (use `const`/`let`)
|
||||
- `eval()` or `Function()` constructor
|
||||
- `with` statement
|
||||
- `arguments` object (use rest parameters)
|
||||
- Deeply nested callbacks (use async/await)
|
||||
- Non-null assertions unless absolutely necessary: `value!`
|
||||
- Type assertions unless necessary: `value as Type`
|
||||
- `@ts-ignore` (use `@ts-expect-error` with comment)
|
||||
|
||||
### Custom ESLint Rules
|
||||
|
||||
**Project-Specific Rules**:
|
||||
- `prefer-static-styles-last` - Static styles property must be last in class
|
||||
- `enforce-umbraco-external-imports` - External dependencies must be imported via `@umbraco-cms/backoffice/external/*`
|
||||
- Private members MUST have leading underscore
|
||||
- Exported types MUST have approved prefix (Umb, Ufm, Manifest, Meta, Example)
|
||||
- Exported string constants MUST have UMB_ prefix
|
||||
- Semicolons required
|
||||
- No `var` keyword
|
||||
- No circular dependencies (max depth 6)
|
||||
- No self-imports
|
||||
- Consistent type imports/exports
|
||||
|
||||
**Allowed JSDoc Tags** (for web-component-analyzer):
|
||||
- `@element` - Element name
|
||||
- `@attr` - HTML attribute
|
||||
- `@fires` - Custom events
|
||||
- `@prop` - Properties
|
||||
- `@slot` - Slots
|
||||
- `@cssprop` - CSS custom properties
|
||||
- `@csspart` - CSS parts
|
||||
|
||||
### Documentation
|
||||
|
||||
- **Public APIs**: JSDoc comments with `@description`, `@param`, `@returns`, `@example`
|
||||
- **Web Components**: JSDoc with web-component-analyzer tags
|
||||
- **Complex logic**: Inline comments explaining "why" not "what"
|
||||
- **TODOs**: Format as `// TODO: description [initials]`
|
||||
- **Deprecated**: Use `@deprecated` tag with migration instructions
|
||||
|
||||
|
||||
279
src/Umbraco.Web.UI.Client/docs/testing.md
Normal file
279
src/Umbraco.Web.UI.Client/docs/testing.md
Normal file
@@ -0,0 +1,279 @@
|
||||
# Testing
|
||||
[← Umbraco Backoffice](../CLAUDE.md) | [← Monorepo Root](../../CLAUDE.md)
|
||||
|
||||
---
|
||||
|
||||
|
||||
### Testing Philosophy
|
||||
|
||||
- **Unit tests** for business logic and utilities
|
||||
- **Component tests** for web components
|
||||
- **Integration tests** for workflows
|
||||
- **E2E tests** for critical user journeys
|
||||
- **Coverage target**: No strict requirement, focus on meaningful tests
|
||||
- **Test pyramid**: Many unit tests, fewer integration tests, few E2E tests
|
||||
|
||||
### Testing Frameworks
|
||||
|
||||
**Unit/Component Testing**:
|
||||
- `@web/test-runner` - Fast test runner for web components
|
||||
- `@open-wc/testing` - Testing utilities, includes Chai assertions
|
||||
- `@types/chai` - Assertion library
|
||||
- `@types/mocha` - Test framework (used by @web/test-runner)
|
||||
- `@web/test-runner-playwright` - Browser launcher
|
||||
- `element-internals-polyfill` - Polyfill for form-associated custom elements
|
||||
|
||||
**E2E Testing**:
|
||||
- `@playwright/test` - End-to-end testing in real browsers
|
||||
- Playwright MSW integration for API mocking
|
||||
|
||||
**Test Utilities**:
|
||||
- `@umbraco-cms/internal/test-utils` - Shared test utilities
|
||||
- MSW (Mock Service Worker) - API mocking
|
||||
- Fixtures in `src/mocks/data/` - Test data
|
||||
|
||||
### Test Project Organization
|
||||
|
||||
```
|
||||
src/
|
||||
├── **/*.test.ts # Unit tests co-located with source
|
||||
├── mocks/
|
||||
│ ├── data/ # Mock data & in-memory databases
|
||||
│ └── handlers/ # MSW request handlers
|
||||
├── examples/
|
||||
│ └── **/*.test.ts # Example tests
|
||||
└── utils/
|
||||
└── test-utils.ts # Shared test utilities
|
||||
|
||||
e2e/
|
||||
├── **/*.spec.ts # Playwright E2E tests
|
||||
└── fixtures/ # E2E test fixtures
|
||||
```
|
||||
|
||||
### Test Naming Convention
|
||||
|
||||
```typescript
|
||||
describe('UmbMyComponent', () => {
|
||||
describe('initialization', () => {
|
||||
it('should create element', async () => {
|
||||
// test
|
||||
});
|
||||
|
||||
it('should set default properties', async () => {
|
||||
// test
|
||||
});
|
||||
});
|
||||
|
||||
describe('user interactions', () => {
|
||||
it('should emit event when button clicked', async () => {
|
||||
// test
|
||||
});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Test Structure (AAA Pattern)
|
||||
|
||||
```typescript
|
||||
it('should do something', async () => {
|
||||
// Arrange - Set up test data and conditions
|
||||
const element = await fixture<UmbMyElement>(html`<umb-my-element></umb-my-element>`);
|
||||
const spy = sinon.spy();
|
||||
element.addEventListener('change', spy);
|
||||
|
||||
// Act - Perform the action
|
||||
element.value = 'new value';
|
||||
await element.updateComplete;
|
||||
|
||||
// Assert - Verify the results
|
||||
expect(spy.calledOnce).to.be.true;
|
||||
expect(element.value).to.equal('new value');
|
||||
});
|
||||
```
|
||||
|
||||
### Unit Test Guidelines
|
||||
|
||||
**What to Test**:
|
||||
- Public API methods and properties
|
||||
- User interactions (clicks, inputs, etc.)
|
||||
- State changes
|
||||
- Event emissions
|
||||
- Error handling
|
||||
- Edge cases
|
||||
|
||||
**What NOT to Test**:
|
||||
- Private implementation details
|
||||
- Framework/library code
|
||||
- Generated code (e.g., OpenAPI clients)
|
||||
- Third-party dependencies
|
||||
|
||||
**Best Practices**:
|
||||
- One assertion per test (when practical)
|
||||
- Test behavior, not implementation
|
||||
- Use meaningful test names (describe what should happen)
|
||||
- Keep tests fast (<100ms each)
|
||||
- Isolate tests (no shared state between tests)
|
||||
- Use fixtures for DOM elements
|
||||
- Use spies/stubs for external dependencies
|
||||
- Clean up after tests (auto-handled by @web/test-runner)
|
||||
|
||||
**Web Component Testing**:
|
||||
|
||||
```typescript
|
||||
import { fixture, html, expect } from '@open-wc/testing';
|
||||
import { UmbMyElement } from './my-element.element';
|
||||
|
||||
describe('UmbMyElement', () => {
|
||||
let element: UmbMyElement;
|
||||
|
||||
beforeEach(async () => {
|
||||
element = await fixture(html`<umb-my-element></umb-my-element>`);
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
expect(element).to.exist;
|
||||
expect(element.shadowRoot).to.exist;
|
||||
});
|
||||
|
||||
it('should be accessible', async () => {
|
||||
await expect(element).to.be.accessible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Integration Test Guidelines
|
||||
|
||||
Integration tests verify interactions between multiple components/systems:
|
||||
|
||||
```typescript
|
||||
describe('UmbContentRepository', () => {
|
||||
let repository: UmbContentRepository;
|
||||
let mockContext: UmbMockContext;
|
||||
|
||||
beforeEach(() => {
|
||||
mockContext = new UmbMockContext();
|
||||
repository = new UmbContentRepository(mockContext);
|
||||
});
|
||||
|
||||
it('should fetch and cache content', async () => {
|
||||
const result = await repository.requestById('content-123');
|
||||
expect(result.data).to.exist;
|
||||
// Verify caching behavior
|
||||
const cached = await repository.requestById('content-123');
|
||||
expect(cached.data).to.equal(result.data);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### E2E Test Guidelines
|
||||
|
||||
E2E tests with Playwright:
|
||||
|
||||
```typescript
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test.describe('Content Editor', () => {
|
||||
test('should create new document', async ({ page }) => {
|
||||
await page.goto('/');
|
||||
await page.click('[data-test="create-document"]');
|
||||
await page.fill('[data-test="document-name"]', 'My New Page');
|
||||
await page.click('[data-test="save"]');
|
||||
|
||||
await expect(page.locator('[data-test="success-notification"]')).toBeVisible();
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
### Mocking Best Practices
|
||||
|
||||
**MSW (Mock Service Worker)** for API mocking:
|
||||
|
||||
```typescript
|
||||
import { rest } from 'msw';
|
||||
|
||||
export const handlers = [
|
||||
rest.get('/umbraco/management/api/v1/document/:id', (req, res, ctx) => {
|
||||
const { id } = req.params;
|
||||
return res(
|
||||
ctx.json({
|
||||
id,
|
||||
name: 'Test Document',
|
||||
// ... mock data
|
||||
})
|
||||
);
|
||||
}),
|
||||
];
|
||||
```
|
||||
|
||||
**Context Mocking**:
|
||||
|
||||
```typescript
|
||||
import { UmbMockContext } from '@umbraco-cms/internal/test-utils';
|
||||
|
||||
const mockContext = new UmbMockContext();
|
||||
mockContext.provideContext(UMB_AUTH_CONTEXT, mockAuthContext);
|
||||
```
|
||||
|
||||
### Running Tests
|
||||
|
||||
**Local Development**:
|
||||
|
||||
```bash
|
||||
# Run all tests once
|
||||
npm test
|
||||
|
||||
# Run in watch mode
|
||||
npm run test:watch
|
||||
|
||||
# Run with dev config (faster, less strict)
|
||||
npm run test:dev
|
||||
|
||||
# Run in watch mode with dev config
|
||||
npm run test:dev-watch
|
||||
|
||||
# Run specific test file pattern
|
||||
npm test -- --files "**/my-component.test.ts"
|
||||
```
|
||||
|
||||
**E2E Tests**:
|
||||
|
||||
```bash
|
||||
# Run E2E tests
|
||||
npm run test:e2e
|
||||
|
||||
# Run in headed mode (see browser)
|
||||
npx playwright test --headed
|
||||
|
||||
# Run specific test
|
||||
npx playwright test e2e/content-editor.spec.ts
|
||||
|
||||
# Debug mode
|
||||
npx playwright test --debug
|
||||
```
|
||||
|
||||
**CI/CD**:
|
||||
- All tests run on pull requests
|
||||
- E2E tests run on Chromium in CI
|
||||
- Retries: 2 attempts on CI, 0 locally
|
||||
- Parallel: Sequential in CI, parallel locally
|
||||
|
||||
### Coverage
|
||||
|
||||
Coverage reporting is currently disabled (see `web-test-runner.config.mjs`):
|
||||
|
||||
```javascript
|
||||
/* TODO: fix coverage report
|
||||
coverageConfig: {
|
||||
reporters: ['lcovonly', 'text-summary'],
|
||||
},
|
||||
*/
|
||||
```
|
||||
|
||||
**What to Exclude from Coverage**:
|
||||
- Test files themselves
|
||||
- Mock data and handlers
|
||||
- Generated code (OpenAPI clients, icons)
|
||||
- External wrapper modules
|
||||
- Type declaration files
|
||||
|
||||
|
||||
Reference in New Issue
Block a user