@salesforce/afv-skills 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -415
- package/package.json +5 -3
- package/skills/building-ui-bundle-app/SKILL.md +325 -0
- package/skills/building-ui-bundle-frontend/SKILL.md +122 -0
- package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/component.md +1 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +169 -0
- package/skills/creating-b2b-commerce-store/references/store-vs-storefront.md +169 -0
- package/skills/deploying-ui-bundle/SKILL.md +77 -0
- package/skills/generating-apex/CREDITS.md +30 -0
- package/skills/generating-apex/SKILL.md +399 -0
- package/skills/generating-apex/assets/abstract.cls +132 -0
- package/skills/generating-apex/assets/batch.cls +125 -0
- package/skills/generating-apex/assets/domain.cls +102 -0
- package/skills/generating-apex/assets/dto.cls +108 -0
- package/skills/generating-apex/assets/exception.cls +51 -0
- package/skills/generating-apex/assets/interface.cls +25 -0
- package/skills/generating-apex/assets/invocable.cls +115 -0
- package/skills/generating-apex/assets/queueable.cls +92 -0
- package/skills/generating-apex/assets/rest-resource.cls +300 -0
- package/skills/generating-apex/assets/schedulable.cls +75 -0
- package/skills/generating-apex/assets/selector.cls +92 -0
- package/skills/generating-apex/assets/service.cls +69 -0
- package/skills/generating-apex/assets/trigger.cls +45 -0
- package/skills/generating-apex/assets/utility.cls +97 -0
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
- package/skills/generating-apex/references/AccountSelector.cls +193 -0
- package/skills/generating-apex/references/AccountService.cls +201 -0
- package/skills/generating-apex-test/CREDITS.md +30 -0
- package/skills/generating-apex-test/SKILL.md +199 -0
- package/skills/generating-apex-test/assets/test-class-template.cls +93 -0
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +111 -0
- package/skills/generating-apex-test/references/assertion-patterns.md +108 -0
- package/skills/generating-apex-test/references/async-testing.md +193 -0
- package/skills/generating-apex-test/references/mocking-patterns.md +220 -0
- package/skills/generating-apex-test/references/test-data-factory.md +75 -0
- package/skills/generating-experience-react-site/SKILL.md +20 -9
- package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
- package/skills/generating-flexipage/SKILL.md +58 -60
- package/skills/generating-ui-bundle-features/SKILL.md +45 -0
- package/skills/generating-ui-bundle-metadata/SKILL.md +106 -0
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/SKILL.md +5 -5
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/constraints.md +2 -2
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/examples.md +1 -1
- package/skills/{implementing-webapp-file-upload → implementing-ui-bundle-file-upload}/SKILL.md +11 -11
- package/skills/searching-media/SKILL.md +342 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data}/SKILL.md +52 -25
- package/skills/using-ui-bundle-salesforce-data/references/mutation-query-generation.md +140 -0
- package/skills/using-ui-bundle-salesforce-data/references/query-testing.md +78 -0
- package/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md +307 -0
- package/skills/using-ui-bundle-salesforce-data/references/schema-introspection.md +53 -0
- package/skills/using-ui-bundle-salesforce-data/references/ui-bundle-integration.md +221 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data/scripts}/graphql-search.sh +75 -23
- package/skills/building-webapp-data-visualization/SKILL.md +0 -72
- package/skills/building-webapp-data-visualization/implementation/bar-line-chart.md +0 -316
- package/skills/building-webapp-data-visualization/implementation/dashboard-layout.md +0 -189
- package/skills/building-webapp-data-visualization/implementation/donut-chart.md +0 -181
- package/skills/building-webapp-data-visualization/implementation/stat-card.md +0 -150
- package/skills/building-webapp-react-components/SKILL.md +0 -96
- package/skills/configuring-webapp-csp-trusted-sites/SKILL.md +0 -90
- package/skills/configuring-webapp-metadata/SKILL.md +0 -158
- package/skills/creating-webapp/SKILL.md +0 -140
- package/skills/deploying-webapp-to-salesforce/SKILL.md +0 -226
- package/skills/installing-webapp-features/SKILL.md +0 -210
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/header-footer.md +0 -0
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/page.md +0 -0
- /package/skills/{configuring-webapp-csp-trusted-sites/implementation/metadata-format.md → generating-ui-bundle-metadata/implementation/csp-metadata-format.md} +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/style-tokens.md +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/troubleshooting.md +0 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Async Testing Patterns
|
|
2
|
+
|
|
3
|
+
## Key Principle
|
|
4
|
+
|
|
5
|
+
`Test.stopTest()` forces all async operations to execute synchronously, allowing assertions on their results.
|
|
6
|
+
|
|
7
|
+
## Batch Apex Testing
|
|
8
|
+
|
|
9
|
+
### Basic Batch Test
|
|
10
|
+
|
|
11
|
+
**Critical:** In test context only one `execute()` invocation runs, so always set `batchSize >= testRecordCount` (e.g., `Database.executeBatch(batch, 200)` with 200 records). Never create more records than the batch size.
|
|
12
|
+
|
|
13
|
+
```apex
|
|
14
|
+
@isTest
|
|
15
|
+
static void shouldProcessAllRecords_WhenBatchExecutes() {
|
|
16
|
+
List<Account> accounts = TestDataFactory.createAccounts(200, true);
|
|
17
|
+
|
|
18
|
+
Test.startTest();
|
|
19
|
+
MyBatchClass batch = new MyBatchClass();
|
|
20
|
+
Id batchId = Database.executeBatch(batch, 200);
|
|
21
|
+
Test.stopTest();
|
|
22
|
+
|
|
23
|
+
List<Account> updated = [SELECT Id, Status__c FROM Account];
|
|
24
|
+
for (Account acc : updated) {
|
|
25
|
+
Assert.areEqual('Processed', acc.Status__c,
|
|
26
|
+
'Batch should update all account statuses');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Batch with Failures
|
|
32
|
+
|
|
33
|
+
```apex
|
|
34
|
+
@isTest
|
|
35
|
+
static void shouldLogErrors_WhenRecordsFail() {
|
|
36
|
+
List<Account> accounts = TestDataFactory.createAccounts(198, true);
|
|
37
|
+
|
|
38
|
+
List<Account> invalidAccounts = new List<Account>();
|
|
39
|
+
for (Integer i = 0; i < 2; i++) {
|
|
40
|
+
invalidAccounts.add(new Account(
|
|
41
|
+
Name = 'Invalid Account ' + i,
|
|
42
|
+
Invalid_Field__c = 'triggers_validation_error'
|
|
43
|
+
));
|
|
44
|
+
}
|
|
45
|
+
insert invalidAccounts;
|
|
46
|
+
|
|
47
|
+
Test.startTest();
|
|
48
|
+
MyBatchClass batch = new MyBatchClass();
|
|
49
|
+
Database.executeBatch(batch, 200);
|
|
50
|
+
Test.stopTest();
|
|
51
|
+
|
|
52
|
+
List<Error_Log__c> errors = [SELECT Id, Message__c FROM Error_Log__c];
|
|
53
|
+
Assert.areEqual(2, errors.size(), 'Should log 2 failed records');
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Batch Chaining
|
|
58
|
+
|
|
59
|
+
Test `finish()` chaining in a **separate test method** — calling `Database.executeBatch()` inside `finish()` during a test can throw `UnexpectedException`. Verify the first batch independently, then test that `finish()` enqueues the next batch.
|
|
60
|
+
|
|
61
|
+
## Queueable Testing
|
|
62
|
+
|
|
63
|
+
### Basic Queueable Test
|
|
64
|
+
|
|
65
|
+
```apex
|
|
66
|
+
@isTest
|
|
67
|
+
static void shouldCompleteProcessing_WhenQueueableEnqueued() {
|
|
68
|
+
Account acc = TestDataFactory.createAccount(true);
|
|
69
|
+
|
|
70
|
+
Test.startTest();
|
|
71
|
+
MyQueueableClass queueable = new MyQueueableClass(acc.Id);
|
|
72
|
+
System.enqueueJob(queueable);
|
|
73
|
+
Test.stopTest();
|
|
74
|
+
|
|
75
|
+
Account updated = [SELECT Id, Status__c FROM Account WHERE Id = :acc.Id];
|
|
76
|
+
Assert.areEqual('Processed', updated.Status__c,
|
|
77
|
+
'Queueable should update account status');
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Queueable Chaining
|
|
82
|
+
|
|
83
|
+
Only the **first** chained queueable executes in tests. Design tests to verify:
|
|
84
|
+
1. First job completes correctly
|
|
85
|
+
2. Chain is properly enqueued (query `AsyncApexJob`)
|
|
86
|
+
3. Each job works independently in its own test method
|
|
87
|
+
|
|
88
|
+
```apex
|
|
89
|
+
@isTest
|
|
90
|
+
static void shouldChainNextJob_WhenMoreRecordsExist() {
|
|
91
|
+
List<Account> accounts = TestDataFactory.createAccounts(500, true);
|
|
92
|
+
|
|
93
|
+
Test.startTest();
|
|
94
|
+
MyChainedQueueable queueable = new MyChainedQueueable(0, 100);
|
|
95
|
+
System.enqueueJob(queueable);
|
|
96
|
+
Test.stopTest();
|
|
97
|
+
|
|
98
|
+
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
|
|
99
|
+
Assert.areEqual(100, processed.size(), 'First batch should process 100 records');
|
|
100
|
+
|
|
101
|
+
List<AsyncApexJob> jobs = [
|
|
102
|
+
SELECT Id, Status, JobType
|
|
103
|
+
FROM AsyncApexJob
|
|
104
|
+
WHERE ApexClass.Name = 'MyChainedQueueable'
|
|
105
|
+
];
|
|
106
|
+
Assert.isTrue(jobs.size() >= 1, 'Chained job should be enqueued');
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Queueable with Callouts
|
|
111
|
+
|
|
112
|
+
Set the callout mock **before** `Test.startTest()`:
|
|
113
|
+
|
|
114
|
+
```apex
|
|
115
|
+
@isTest
|
|
116
|
+
static void shouldMakeCallout_WhenQueueableWithCallout() {
|
|
117
|
+
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, '{"status":"ok"}'));
|
|
118
|
+
Account acc = TestDataFactory.createAccount(true);
|
|
119
|
+
|
|
120
|
+
Test.startTest();
|
|
121
|
+
MyQueueableWithCallout queueable = new MyQueueableWithCallout(acc.Id);
|
|
122
|
+
System.enqueueJob(queueable);
|
|
123
|
+
Test.stopTest();
|
|
124
|
+
|
|
125
|
+
Account updated = [SELECT Id, External_Status__c FROM Account WHERE Id = :acc.Id];
|
|
126
|
+
Assert.areEqual('Synced', updated.External_Status__c,
|
|
127
|
+
'Should update status after successful callout');
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Future Method Testing
|
|
132
|
+
|
|
133
|
+
```apex
|
|
134
|
+
@isTest
|
|
135
|
+
static void shouldExecuteFutureMethod() {
|
|
136
|
+
Account acc = TestDataFactory.createAccount(true);
|
|
137
|
+
|
|
138
|
+
Test.startTest();
|
|
139
|
+
MyClass.processFuture(acc.Id);
|
|
140
|
+
Test.stopTest();
|
|
141
|
+
|
|
142
|
+
Account updated = [SELECT Id, Processed__c FROM Account WHERE Id = :acc.Id];
|
|
143
|
+
Assert.areEqual(true, updated.Processed__c, 'Future should process record');
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Scheduled Apex Testing
|
|
148
|
+
|
|
149
|
+
### Direct Execution
|
|
150
|
+
|
|
151
|
+
```apex
|
|
152
|
+
@isTest
|
|
153
|
+
static void shouldExecuteScheduledJob() {
|
|
154
|
+
List<Account> accounts = TestDataFactory.createAccounts(50, true);
|
|
155
|
+
|
|
156
|
+
Test.startTest();
|
|
157
|
+
MyScheduledClass scheduled = new MyScheduledClass();
|
|
158
|
+
scheduled.execute(null);
|
|
159
|
+
Test.stopTest();
|
|
160
|
+
|
|
161
|
+
List<Account> processed = [SELECT Id FROM Account WHERE Processed__c = true];
|
|
162
|
+
Assert.areEqual(50, processed.size(), 'Scheduled job should process records');
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
### CRON Registration
|
|
167
|
+
|
|
168
|
+
```apex
|
|
169
|
+
@isTest
|
|
170
|
+
static void shouldScheduleJob() {
|
|
171
|
+
Test.startTest();
|
|
172
|
+
String cronExp = '0 0 6 * * ?';
|
|
173
|
+
String jobId = System.schedule('Daily Processing', cronExp, new MyScheduledClass());
|
|
174
|
+
Test.stopTest();
|
|
175
|
+
|
|
176
|
+
CronTrigger ct = [
|
|
177
|
+
SELECT Id, CronExpression, State
|
|
178
|
+
FROM CronTrigger
|
|
179
|
+
WHERE Id = :jobId
|
|
180
|
+
];
|
|
181
|
+
Assert.areEqual('0 0 6 * * ?', ct.CronExpression, 'CRON should match');
|
|
182
|
+
Assert.areEqual('WAITING', ct.State, 'Job should be waiting');
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Common Pitfalls
|
|
187
|
+
|
|
188
|
+
| Pitfall | Impact |
|
|
189
|
+
|---|---|
|
|
190
|
+
| Missing `Test.stopTest()` | Async never executes, assertions fail silently |
|
|
191
|
+
| Expecting all chained queueables to run | Only the first runs; test each independently |
|
|
192
|
+
| Mock set after `Test.startTest()` | Callout mock must be set **before** `Test.startTest()` |
|
|
193
|
+
| Batch size < record count in tests | Only `batchSize` records processed; set `batchSize >= recordCount` |
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Mocking Patterns
|
|
2
|
+
|
|
3
|
+
## HTTP Callout Mocking
|
|
4
|
+
|
|
5
|
+
Apex doesn't allow real HTTP callouts in tests. Use `HttpCalloutMock` interface.
|
|
6
|
+
|
|
7
|
+
### Basic Mock
|
|
8
|
+
|
|
9
|
+
```apex
|
|
10
|
+
@isTest
|
|
11
|
+
public class MockHttpResponse implements HttpCalloutMock {
|
|
12
|
+
|
|
13
|
+
private Integer statusCode;
|
|
14
|
+
private String body;
|
|
15
|
+
|
|
16
|
+
public MockHttpResponse(Integer statusCode, String body) {
|
|
17
|
+
this.statusCode = statusCode;
|
|
18
|
+
this.body = body;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public HTTPResponse respond(HTTPRequest req) {
|
|
22
|
+
HttpResponse res = new HttpResponse();
|
|
23
|
+
res.setStatusCode(statusCode);
|
|
24
|
+
res.setBody(body);
|
|
25
|
+
res.setHeader('Content-Type', 'application/json');
|
|
26
|
+
return res;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Using the Mock
|
|
32
|
+
|
|
33
|
+
```apex
|
|
34
|
+
@isTest
|
|
35
|
+
static void shouldProcessApiResponse_WhenCalloutSucceeds() {
|
|
36
|
+
String mockResponse = '{"status": "success", "data": [{"id": "123"}]}';
|
|
37
|
+
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockResponse));
|
|
38
|
+
|
|
39
|
+
Test.startTest();
|
|
40
|
+
List<ExternalRecord> results = MyIntegrationService.fetchRecords();
|
|
41
|
+
Test.stopTest();
|
|
42
|
+
|
|
43
|
+
Assert.areEqual(1, results.size(), 'Should parse one record from response');
|
|
44
|
+
Assert.areEqual('123', results[0].externalId, 'Should extract correct ID');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
@isTest
|
|
48
|
+
static void shouldHandleError_WhenCalloutFails() {
|
|
49
|
+
String errorResponse = '{"error": "Unauthorized"}';
|
|
50
|
+
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(401, errorResponse));
|
|
51
|
+
|
|
52
|
+
Test.startTest();
|
|
53
|
+
CalloutResult result = MyIntegrationService.fetchRecords();
|
|
54
|
+
Test.stopTest();
|
|
55
|
+
|
|
56
|
+
Assert.areEqual(false, result.isSuccess, 'Should indicate failure');
|
|
57
|
+
Assert.isTrue(result.errorMessage.contains('Unauthorized'), 'Should capture error');
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Multi-Request Mock
|
|
62
|
+
|
|
63
|
+
For services making multiple callouts:
|
|
64
|
+
|
|
65
|
+
```apex
|
|
66
|
+
@isTest
|
|
67
|
+
public class MultiRequestMock implements HttpCalloutMock {
|
|
68
|
+
|
|
69
|
+
private Map<String, HttpResponse> endpointResponses;
|
|
70
|
+
|
|
71
|
+
public MultiRequestMock(Map<String, HttpResponse> responses) {
|
|
72
|
+
this.endpointResponses = responses;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
public HTTPResponse respond(HTTPRequest req) {
|
|
76
|
+
String endpoint = req.getEndpoint();
|
|
77
|
+
for (String key : endpointResponses.keySet()) {
|
|
78
|
+
if (endpoint.contains(key)) {
|
|
79
|
+
return endpointResponses.get(key);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
HttpResponse res = new HttpResponse();
|
|
83
|
+
res.setStatusCode(404);
|
|
84
|
+
res.setBody('{"error": "Not found"}');
|
|
85
|
+
return res;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### StaticResourceCalloutMock
|
|
91
|
+
|
|
92
|
+
Use when response JSON is large or complex:
|
|
93
|
+
|
|
94
|
+
```apex
|
|
95
|
+
@isTest
|
|
96
|
+
static void shouldParseComplexResponse() {
|
|
97
|
+
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
|
|
98
|
+
mock.setStaticResource('TestApiResponse');
|
|
99
|
+
mock.setStatusCode(200);
|
|
100
|
+
mock.setHeader('Content-Type', 'application/json');
|
|
101
|
+
Test.setMock(HttpCalloutMock.class, mock);
|
|
102
|
+
|
|
103
|
+
Test.startTest();
|
|
104
|
+
Result r = MyService.callExternalApi();
|
|
105
|
+
Test.stopTest();
|
|
106
|
+
|
|
107
|
+
Assert.isNotNull(r, 'Should parse response');
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## SOSL Mocking
|
|
112
|
+
|
|
113
|
+
SOSL returns empty results in tests by default. Call `Test.setFixedSearchResults(List<Id>)` before the search:
|
|
114
|
+
|
|
115
|
+
```apex
|
|
116
|
+
@isTest
|
|
117
|
+
static void shouldReturnSearchResults() {
|
|
118
|
+
Account acc = TestDataFactory.createAccount(true);
|
|
119
|
+
Test.setFixedSearchResults(new List<Id>{ acc.Id });
|
|
120
|
+
|
|
121
|
+
Test.startTest();
|
|
122
|
+
List<Account> results = MyService.searchAccounts('Test');
|
|
123
|
+
Test.stopTest();
|
|
124
|
+
|
|
125
|
+
Assert.areEqual(1, results.size(), 'Should return mocked search result');
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## DML Mocking (Constructor Injection)
|
|
130
|
+
|
|
131
|
+
Design for testability — use a public constructor for production and a `@TestVisible private` constructor that accepts mock interfaces:
|
|
132
|
+
|
|
133
|
+
```apex
|
|
134
|
+
public class MyService {
|
|
135
|
+
private IDML dmlHandler;
|
|
136
|
+
|
|
137
|
+
public MyService() {
|
|
138
|
+
this.dmlHandler = new DMLHandler();
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@TestVisible
|
|
142
|
+
private MyService(IDML dmlHandler) {
|
|
143
|
+
this.dmlHandler = dmlHandler;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
public void createRecords(List<Account> accounts) {
|
|
147
|
+
dmlHandler.doInsert(accounts);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Stub API (System.StubProvider)
|
|
153
|
+
|
|
154
|
+
For mocking Apex class dependencies:
|
|
155
|
+
|
|
156
|
+
```apex
|
|
157
|
+
@isTest
|
|
158
|
+
public class MyServiceMock implements System.StubProvider {
|
|
159
|
+
|
|
160
|
+
public Object handleMethodCall(
|
|
161
|
+
Object stubbedObject, String stubbedMethodName, Type returnType,
|
|
162
|
+
List<Type> paramTypes, List<String> paramNames, List<Object> args
|
|
163
|
+
) {
|
|
164
|
+
if (stubbedMethodName == 'getAccountData') {
|
|
165
|
+
return new AccountData('Mock Account', 'Active');
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
@isTest
|
|
172
|
+
static void shouldUseAccountData() {
|
|
173
|
+
MyServiceMock mockProvider = new MyServiceMock();
|
|
174
|
+
IMyService mockService = (IMyService) Test.createStub(IMyService.class, mockProvider);
|
|
175
|
+
|
|
176
|
+
MyController controller = new MyController(mockService);
|
|
177
|
+
|
|
178
|
+
Test.startTest();
|
|
179
|
+
String result = controller.displayAccountInfo();
|
|
180
|
+
Test.stopTest();
|
|
181
|
+
|
|
182
|
+
Assert.isTrue(result.contains('Mock Account'), 'Should use mocked data');
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Email Testing
|
|
187
|
+
|
|
188
|
+
Apex doesn't actually send emails in tests. Use limits to verify:
|
|
189
|
+
|
|
190
|
+
```apex
|
|
191
|
+
@isTest
|
|
192
|
+
static void shouldSendEmail_WhenTriggered() {
|
|
193
|
+
Integer emailsBefore = Limits.getEmailInvocations();
|
|
194
|
+
|
|
195
|
+
Test.startTest();
|
|
196
|
+
MyService.sendNotification(testContact);
|
|
197
|
+
Test.stopTest();
|
|
198
|
+
|
|
199
|
+
Assert.areEqual(emailsBefore + 1, Limits.getEmailInvocations(),
|
|
200
|
+
'One email should be sent');
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Platform Event Testing
|
|
205
|
+
|
|
206
|
+
```apex
|
|
207
|
+
@isTest
|
|
208
|
+
static void shouldPublishEvent_WhenRecordCreated() {
|
|
209
|
+
Test.startTest();
|
|
210
|
+
Test.enableChangeDataCapture();
|
|
211
|
+
|
|
212
|
+
Account acc = TestDataFactory.createAccount(true);
|
|
213
|
+
Test.getEventBus().deliver();
|
|
214
|
+
Test.stopTest();
|
|
215
|
+
|
|
216
|
+
// Query platform event trigger results
|
|
217
|
+
List<EventLog__c> logs = [SELECT Id FROM EventLog__c WHERE AccountId__c = :acc.Id];
|
|
218
|
+
Assert.areEqual(1, logs.size(), 'Event handler should create log record');
|
|
219
|
+
}
|
|
220
|
+
```
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# TestDataFactory Patterns
|
|
2
|
+
|
|
3
|
+
For the base class template, see [assets/test-data-factory-template.cls](../assets/test-data-factory-template.cls).
|
|
4
|
+
|
|
5
|
+
## Design Rules
|
|
6
|
+
|
|
7
|
+
1. **Always accept a `doInsert` flag** — lets callers modify records before insert
|
|
8
|
+
2. **Append loop index to all fields that participate in matching rules** — prevents `DUPLICATES_DETECTED` errors from active Duplicate Rules
|
|
9
|
+
3. **Single-record methods delegate to bulk** — e.g., `createAccount(doInsert)` calls `createAccounts(1, doInsert)[0]`
|
|
10
|
+
4. **Return created records** — enables chaining and further manipulation
|
|
11
|
+
5. **Set all required fields** — include fields enforced by validation rules, not just schema-required fields
|
|
12
|
+
|
|
13
|
+
## Field Override Pattern
|
|
14
|
+
|
|
15
|
+
Allow callers to override default values without creating new factory methods:
|
|
16
|
+
|
|
17
|
+
```apex
|
|
18
|
+
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
|
|
19
|
+
Account acc = new Account(
|
|
20
|
+
Name = 'Test Account',
|
|
21
|
+
Industry = 'Technology'
|
|
22
|
+
);
|
|
23
|
+
for (String fieldName : fieldOverrides.keySet()) {
|
|
24
|
+
acc.put(fieldName, fieldOverrides.get(fieldName));
|
|
25
|
+
}
|
|
26
|
+
if (doInsert) insert acc;
|
|
27
|
+
return acc;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Usage:
|
|
31
|
+
Account acc = TestDataFactory.createAccount(new Map<String, Object>{
|
|
32
|
+
'Name' => 'Custom Name',
|
|
33
|
+
'Industry' => 'Healthcare'
|
|
34
|
+
}, true);
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Record Type Support
|
|
38
|
+
|
|
39
|
+
```apex
|
|
40
|
+
public static Account createAccountByRecordType(String recordTypeName, Boolean doInsert) {
|
|
41
|
+
Id recordTypeId = Schema.SObjectType.Account
|
|
42
|
+
.getRecordTypeInfosByDeveloperName()
|
|
43
|
+
.get(recordTypeName)
|
|
44
|
+
.getRecordTypeId();
|
|
45
|
+
|
|
46
|
+
Account acc = new Account(
|
|
47
|
+
Name = 'Test Account',
|
|
48
|
+
RecordTypeId = recordTypeId
|
|
49
|
+
);
|
|
50
|
+
if (doInsert) insert acc;
|
|
51
|
+
return acc;
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Handling Duplicate Rules
|
|
56
|
+
|
|
57
|
+
When unique field values alone are not sufficient, use `Database.insert()` with a `DuplicateRuleHeader`:
|
|
58
|
+
|
|
59
|
+
```apex
|
|
60
|
+
public static List<Account> createAccountsAllowDuplicates(Integer count, Boolean doInsert) {
|
|
61
|
+
List<Account> accounts = new List<Account>();
|
|
62
|
+
for (Integer i = 0; i < count; i++) {
|
|
63
|
+
accounts.add(new Account(
|
|
64
|
+
Name = 'Test Account ' + i,
|
|
65
|
+
Phone = '555-000-' + String.valueOf(i).leftPad(4, '0')
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
if (doInsert) {
|
|
69
|
+
Database.DMLOptions dml = new Database.DMLOptions();
|
|
70
|
+
dml.DuplicateRuleHeader.allowSave = true;
|
|
71
|
+
Database.insert(accounts, dml);
|
|
72
|
+
}
|
|
73
|
+
return accounts;
|
|
74
|
+
}
|
|
75
|
+
```
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: generating-experience-react-site
|
|
3
|
-
description: "Use this skill when users need to create or configure a Salesforce Digital Experience Site specifically for hosting a React
|
|
3
|
+
description: "Use this skill when users need to create or configure a Salesforce Digital Experience Site specifically for hosting a React UI bundle. Trigger when users mention creating an Experience site for a React app, setting up a React site on Salesforce, configuring Network/CustomSite/DigitalExperience metadata for a UI bundle, or deploying site infrastructure for a React application. Also trigger when users mention site URL path prefixes, app namespaces, appDevName, guest access configuration, DigitalExperienceConfig, DigitalExperienceBundle, or sfdc_cms__site content types in the context of React apps. Always use this skill for any React UI bundle site creation or site infrastructure configuration work, even if the user just says \"create a site for my React app\" or \"set up the site for my UI bundle.\""
|
|
4
4
|
---
|
|
5
5
|
|
|
6
|
-
# Digital Experience Site for React
|
|
7
|
-
Create and configure Digital Experience Sites that host React
|
|
6
|
+
# Digital Experience Site for React UI Bundles
|
|
7
|
+
Create and configure Digital Experience Sites that host React UI bundles on Salesforce. This skill generates the minimum necessary site infrastructure — Network, CustomSite, DigitalExperienceConfig, DigitalExperienceBundle, and the `sfdc_cms__site` content type — so a React app can be served from Salesforce.
|
|
8
8
|
|
|
9
|
-
React sites differ from standard LWR sites: they don't need routes, views, theme layouts, or branding sets. The site acts as a thin container (`appContainer: true`) that delegates rendering to the React
|
|
9
|
+
React sites differ from standard LWR sites: they don't need routes, views, theme layouts, or branding sets. The site acts as a thin container (`appContainer: true`) that delegates rendering to the React UI bundle referenced by `appSpace`.
|
|
10
10
|
|
|
11
11
|
## Required Properties
|
|
12
12
|
Resolve all five properties before generating any metadata. Each has a fallback chain — work through each option in order until a value is found.
|
|
@@ -14,9 +14,9 @@ Resolve all five properties before generating any metadata. Each has a fallback
|
|
|
14
14
|
| Property | Format | How to Resolve |
|
|
15
15
|
|----------|--------|----------------|
|
|
16
16
|
| **siteName** | `UpperCamelCase` (e.g., `MyCommunity`) | Ask user or derive from context |
|
|
17
|
-
| **siteUrlPathPrefix** | `
|
|
17
|
+
| **siteUrlPathPrefix** | `All lowercase` (e.g., `mycommunity`) | User-provided, or convert siteName to all lowercase with alphanumeric characters only |
|
|
18
18
|
| **appNamespace** | String | `namespace` in `sfdx-project.json` → `sf data query -q "SELECT NamespacePrefix FROM Organization" --target-org ${usernameOrAlias}` → default `c` |
|
|
19
|
-
| **appDevName** | String | `
|
|
19
|
+
| **appDevName** | String | `UIBundle` metadata in the project → `sf data query -q "SELECT DeveloperName FROM UIBundle" --target-org ${usernameOrAlias}` → default to siteName |
|
|
20
20
|
| **enableGuestAccess** | Boolean | Ask user whether unauthenticated guest users can access site APIs → default `false` |
|
|
21
21
|
|
|
22
22
|
The `appNamespace` and `appDevName` properties connect the site to the correct React application. Getting these wrong means the site deploys but shows a blank page, so take care to resolve them from real project data.
|
|
@@ -26,7 +26,7 @@ The `appNamespace` and `appDevName` properties connect the site to the correct R
|
|
|
26
26
|
Determine values for all five properties before constructing anything. Use the resolution strategies in the table above, falling through each option until a value is found.
|
|
27
27
|
|
|
28
28
|
### Step 2: Create the Project Structure
|
|
29
|
-
|
|
29
|
+
Use available Salesforce metadata schema and field context for `Network`, `CustomSite`, `DigitalExperienceConfig`, and `DigitalExperienceBundle` to ensure each file uses valid structure.
|
|
30
30
|
|
|
31
31
|
Create any files and directories that don't already exist, using these paths:
|
|
32
32
|
|
|
@@ -51,8 +51,19 @@ Use the default templates in the docs below. Values in `{braces}` are resolved p
|
|
|
51
51
|
| DigitalExperienceBundle | [configure-metadata-digital-experience-bundle.md](docs/configure-metadata-digital-experience-bundle.md) |
|
|
52
52
|
| DigitalExperience (sfdc_cms__site) | [configure-metadata-digital-experience.md](docs/configure-metadata-digital-experience.md) |
|
|
53
53
|
|
|
54
|
+
### Execution Note for Step 3: Load and use the docs
|
|
55
|
+
- Agents MUST read the full contents of each docs/*.md file referenced in Step 3 before attempting to populate metadata fields.
|
|
56
|
+
- Use your platform's file-read tool (for example, `read_file`) to load these files in full, then perform placeholder substitution for values in `{braces}` using the resolved properties from Step 1.
|
|
57
|
+
- Files to load:
|
|
58
|
+
- `docs/configure-metadata-network.md`
|
|
59
|
+
- `docs/configure-metadata-custom-site.md`
|
|
60
|
+
- `docs/configure-metadata-digital-experience-config.md`
|
|
61
|
+
- `docs/configure-metadata-digital-experience-bundle.md`
|
|
62
|
+
- `docs/configure-metadata-digital-experience.md`
|
|
63
|
+
- Read entire file contents, replace placeholders (e.g. `{siteName}`) with the resolved values, then use the expanded templates to populate the metadata XML/JSON content.
|
|
64
|
+
|
|
54
65
|
### Step 4: Resolve Additional Configurations
|
|
55
|
-
Address any extra configurations the user requests. Use the
|
|
66
|
+
Address any extra configurations the user requests. Use the metadata sections and field context identified in Step 2 to understand each field’s purpose and constraints, then update only the minimum necessary fields.
|
|
56
67
|
|
|
57
68
|
## Verification Checklist
|
|
58
69
|
Before deploying, confirm:
|
|
@@ -60,7 +71,7 @@ Before deploying, confirm:
|
|
|
60
71
|
- [ ] All five required properties are resolved
|
|
61
72
|
- [ ] All metadata directories and files exist per the project structure
|
|
62
73
|
- [ ] All metadata fields are populated per the templates and user requests
|
|
63
|
-
- [ ] `appSpace` in `content.json` matches an existing `
|
|
74
|
+
- [ ] `appSpace` in `content.json` matches an existing `UIBundle` metadata record
|
|
64
75
|
- [ ] Deployment validates successfully:
|
|
65
76
|
```bash
|
|
66
77
|
sf project deploy validate --metadata Network CustomSite DigitalExperienceConfig DigitalExperienceBundle DigitalExperience --target-org ${usernameOrAlias}
|
package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Purpose
|
|
4
4
|
These configuration files create **net-new, default** DigitalExperience content records (`sfdc_cms__site` type) for a Digital Experience React Site. They are not intended to edit or modify existing DigitalExperience content. Use these templates only when provisioning a brand-new React site.
|
|
5
5
|
|
|
6
|
-
The `appContainer: true` and `appSpace` fields in `content.json` are what make this a React site rather than a standard LWR site. The `appSpace` value follows the format `{namespace}__{developerName}` and must match a deployed `
|
|
6
|
+
The `appContainer: true` and `appSpace` fields in `content.json` are what make this a React site rather than a standard LWR site. The `appSpace` value follows the format `{namespace}__{developerName}` and must match a deployed `UIBundle` metadata record.
|
|
7
7
|
|
|
8
8
|
## File Location
|
|
9
9
|
The DigitalExperience directory contains only `_meta.json` and `content.json`. Do not create any directories other than `sfdc_cms__site` inside the bundle.
|