@salesforce/afv-skills 1.5.1 → 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 +335 -189
- package/skills/generating-apex/assets/abstract.cls +12 -8
- package/skills/generating-apex/assets/batch.cls +7 -7
- package/skills/generating-apex/assets/domain.cls +5 -5
- package/skills/generating-apex/assets/dto.cls +11 -11
- package/skills/generating-apex/assets/exception.cls +1 -1
- package/skills/generating-apex/assets/interface.cls +2 -2
- package/skills/generating-apex/assets/invocable.cls +115 -0
- package/skills/generating-apex/assets/queueable.cls +6 -6
- package/skills/generating-apex/assets/rest-resource.cls +300 -0
- package/skills/generating-apex/assets/schedulable.cls +7 -7
- package/skills/generating-apex/assets/selector.cls +7 -7
- package/skills/generating-apex/assets/service.cls +4 -4
- package/skills/generating-apex/assets/trigger.cls +45 -0
- package/skills/generating-apex/assets/utility.cls +5 -5
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +7 -7
- package/skills/generating-apex/references/AccountSelector.cls +10 -10
- package/skills/generating-apex/references/AccountService.cls +9 -9
- package/skills/generating-apex-test/CREDITS.md +30 -0
- package/skills/generating-apex-test/SKILL.md +165 -74
- package/skills/generating-apex-test/assets/test-class-template.cls +23 -54
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +0 -1
- package/skills/generating-apex-test/references/assertion-patterns.md +38 -95
- package/skills/generating-apex-test/references/async-testing.md +59 -142
- package/skills/generating-apex-test/references/mocking-patterns.md +77 -76
- package/skills/generating-apex-test/references/test-data-factory.md +29 -130
- package/skills/generating-experience-react-site/SKILL.md +9 -9
- package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
- package/skills/generating-flexipage/SKILL.md +28 -12
- 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 +1 -1
- 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 -138
- 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
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
Apex doesn't allow real HTTP callouts in tests. Use `HttpCalloutMock` interface.
|
|
6
6
|
|
|
7
|
-
### Basic Mock
|
|
7
|
+
### Basic Mock
|
|
8
8
|
|
|
9
9
|
```apex
|
|
10
10
|
@isTest
|
|
@@ -33,34 +33,28 @@ public class MockHttpResponse implements HttpCalloutMock {
|
|
|
33
33
|
```apex
|
|
34
34
|
@isTest
|
|
35
35
|
static void shouldProcessApiResponse_WhenCalloutSucceeds() {
|
|
36
|
-
// Given
|
|
37
36
|
String mockResponse = '{"status": "success", "data": [{"id": "123"}]}';
|
|
38
37
|
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(200, mockResponse));
|
|
39
|
-
|
|
40
|
-
// When
|
|
38
|
+
|
|
41
39
|
Test.startTest();
|
|
42
40
|
List<ExternalRecord> results = MyIntegrationService.fetchRecords();
|
|
43
41
|
Test.stopTest();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
System.assertEquals('123', results[0].externalId, 'Should extract correct ID');
|
|
42
|
+
|
|
43
|
+
Assert.areEqual(1, results.size(), 'Should parse one record from response');
|
|
44
|
+
Assert.areEqual('123', results[0].externalId, 'Should extract correct ID');
|
|
48
45
|
}
|
|
49
46
|
|
|
50
47
|
@isTest
|
|
51
48
|
static void shouldHandleError_WhenCalloutFails() {
|
|
52
|
-
// Given
|
|
53
49
|
String errorResponse = '{"error": "Unauthorized"}';
|
|
54
50
|
Test.setMock(HttpCalloutMock.class, new MockHttpResponse(401, errorResponse));
|
|
55
|
-
|
|
56
|
-
// When
|
|
51
|
+
|
|
57
52
|
Test.startTest();
|
|
58
53
|
CalloutResult result = MyIntegrationService.fetchRecords();
|
|
59
54
|
Test.stopTest();
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
System.assert(result.errorMessage.contains('Unauthorized'), 'Should capture error');
|
|
55
|
+
|
|
56
|
+
Assert.areEqual(false, result.isSuccess, 'Should indicate failure');
|
|
57
|
+
Assert.isTrue(result.errorMessage.contains('Unauthorized'), 'Should capture error');
|
|
64
58
|
}
|
|
65
59
|
```
|
|
66
60
|
|
|
@@ -71,83 +65,101 @@ For services making multiple callouts:
|
|
|
71
65
|
```apex
|
|
72
66
|
@isTest
|
|
73
67
|
public class MultiRequestMock implements HttpCalloutMock {
|
|
74
|
-
|
|
68
|
+
|
|
75
69
|
private Map<String, HttpResponse> endpointResponses;
|
|
76
|
-
|
|
70
|
+
|
|
77
71
|
public MultiRequestMock(Map<String, HttpResponse> responses) {
|
|
78
72
|
this.endpointResponses = responses;
|
|
79
73
|
}
|
|
80
|
-
|
|
74
|
+
|
|
81
75
|
public HTTPResponse respond(HTTPRequest req) {
|
|
82
76
|
String endpoint = req.getEndpoint();
|
|
83
|
-
|
|
84
77
|
for (String key : endpointResponses.keySet()) {
|
|
85
78
|
if (endpoint.contains(key)) {
|
|
86
79
|
return endpointResponses.get(key);
|
|
87
80
|
}
|
|
88
81
|
}
|
|
89
|
-
|
|
90
|
-
// Default 404 if no match
|
|
91
82
|
HttpResponse res = new HttpResponse();
|
|
92
83
|
res.setStatusCode(404);
|
|
93
84
|
res.setBody('{"error": "Not found"}');
|
|
94
85
|
return res;
|
|
95
86
|
}
|
|
96
87
|
}
|
|
97
|
-
|
|
98
|
-
// Usage:
|
|
99
|
-
Map<String, HttpResponse> mocks = new Map<String, HttpResponse>();
|
|
100
|
-
|
|
101
|
-
HttpResponse authResponse = new HttpResponse();
|
|
102
|
-
authResponse.setStatusCode(200);
|
|
103
|
-
authResponse.setBody('{"token": "abc123"}');
|
|
104
|
-
mocks.put('/oauth/token', authResponse);
|
|
105
|
-
|
|
106
|
-
HttpResponse dataResponse = new HttpResponse();
|
|
107
|
-
dataResponse.setStatusCode(200);
|
|
108
|
-
dataResponse.setBody('{"records": []}');
|
|
109
|
-
mocks.put('/api/records', dataResponse);
|
|
110
|
-
|
|
111
|
-
Test.setMock(HttpCalloutMock.class, new MultiRequestMock(mocks));
|
|
112
88
|
```
|
|
113
89
|
|
|
114
|
-
|
|
90
|
+
### StaticResourceCalloutMock
|
|
115
91
|
|
|
116
|
-
|
|
92
|
+
Use when response JSON is large or complex:
|
|
117
93
|
|
|
118
94
|
```apex
|
|
119
95
|
@isTest
|
|
120
96
|
static void shouldParseComplexResponse() {
|
|
121
97
|
StaticResourceCalloutMock mock = new StaticResourceCalloutMock();
|
|
122
|
-
mock.setStaticResource('TestApiResponse');
|
|
98
|
+
mock.setStaticResource('TestApiResponse');
|
|
123
99
|
mock.setStatusCode(200);
|
|
124
100
|
mock.setHeader('Content-Type', 'application/json');
|
|
125
|
-
|
|
126
101
|
Test.setMock(HttpCalloutMock.class, mock);
|
|
127
|
-
|
|
102
|
+
|
|
128
103
|
Test.startTest();
|
|
129
104
|
Result r = MyService.callExternalApi();
|
|
130
105
|
Test.stopTest();
|
|
131
|
-
|
|
132
|
-
|
|
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
|
+
}
|
|
133
149
|
}
|
|
134
150
|
```
|
|
135
151
|
|
|
136
|
-
## Stub API (
|
|
152
|
+
## Stub API (System.StubProvider)
|
|
137
153
|
|
|
138
|
-
For mocking Apex class dependencies
|
|
154
|
+
For mocking Apex class dependencies:
|
|
139
155
|
|
|
140
156
|
```apex
|
|
141
157
|
@isTest
|
|
142
158
|
public class MyServiceMock implements System.StubProvider {
|
|
143
|
-
|
|
159
|
+
|
|
144
160
|
public Object handleMethodCall(
|
|
145
|
-
Object stubbedObject,
|
|
146
|
-
String
|
|
147
|
-
Type returnType,
|
|
148
|
-
List<Type> paramTypes,
|
|
149
|
-
List<String> paramNames,
|
|
150
|
-
List<Object> args
|
|
161
|
+
Object stubbedObject, String stubbedMethodName, Type returnType,
|
|
162
|
+
List<Type> paramTypes, List<String> paramNames, List<Object> args
|
|
151
163
|
) {
|
|
152
164
|
if (stubbedMethodName == 'getAccountData') {
|
|
153
165
|
return new AccountData('Mock Account', 'Active');
|
|
@@ -156,42 +168,36 @@ public class MyServiceMock implements System.StubProvider {
|
|
|
156
168
|
}
|
|
157
169
|
}
|
|
158
170
|
|
|
159
|
-
// Usage in test:
|
|
160
171
|
@isTest
|
|
161
172
|
static void shouldUseAccountData() {
|
|
162
173
|
MyServiceMock mockProvider = new MyServiceMock();
|
|
163
|
-
IMyService mockService = (IMyService)Test.createStub(IMyService.class, mockProvider);
|
|
164
|
-
|
|
165
|
-
// Inject mock into class under test
|
|
174
|
+
IMyService mockService = (IMyService) Test.createStub(IMyService.class, mockProvider);
|
|
175
|
+
|
|
166
176
|
MyController controller = new MyController(mockService);
|
|
167
|
-
|
|
177
|
+
|
|
168
178
|
Test.startTest();
|
|
169
179
|
String result = controller.displayAccountInfo();
|
|
170
180
|
Test.stopTest();
|
|
171
|
-
|
|
172
|
-
|
|
181
|
+
|
|
182
|
+
Assert.isTrue(result.contains('Mock Account'), 'Should use mocked data');
|
|
173
183
|
}
|
|
174
184
|
```
|
|
175
185
|
|
|
176
|
-
## Email
|
|
186
|
+
## Email Testing
|
|
177
187
|
|
|
178
|
-
Apex
|
|
188
|
+
Apex doesn't actually send emails in tests. Use limits to verify:
|
|
179
189
|
|
|
180
190
|
```apex
|
|
181
191
|
@isTest
|
|
182
192
|
static void shouldSendEmail_WhenTriggered() {
|
|
183
193
|
Integer emailsBefore = Limits.getEmailInvocations();
|
|
184
|
-
|
|
194
|
+
|
|
185
195
|
Test.startTest();
|
|
186
196
|
MyService.sendNotification(testContact);
|
|
187
197
|
Test.stopTest();
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
emailsBefore + 1,
|
|
192
|
-
Limits.getEmailInvocations(),
|
|
193
|
-
'One email should be sent'
|
|
194
|
-
);
|
|
198
|
+
|
|
199
|
+
Assert.areEqual(emailsBefore + 1, Limits.getEmailInvocations(),
|
|
200
|
+
'One email should be sent');
|
|
195
201
|
}
|
|
196
202
|
```
|
|
197
203
|
|
|
@@ -201,19 +207,14 @@ static void shouldSendEmail_WhenTriggered() {
|
|
|
201
207
|
@isTest
|
|
202
208
|
static void shouldPublishEvent_WhenRecordCreated() {
|
|
203
209
|
Test.startTest();
|
|
204
|
-
|
|
205
|
-
// Enable event delivery in test context
|
|
206
210
|
Test.enableChangeDataCapture();
|
|
207
|
-
|
|
211
|
+
|
|
208
212
|
Account acc = TestDataFactory.createAccount(true);
|
|
209
|
-
|
|
210
|
-
// Deliver events
|
|
211
213
|
Test.getEventBus().deliver();
|
|
212
|
-
|
|
213
214
|
Test.stopTest();
|
|
214
215
|
|
|
215
216
|
// Query platform event trigger results
|
|
216
217
|
List<EventLog__c> logs = [SELECT Id FROM EventLog__c WHERE AccountId__c = :acc.Id];
|
|
217
|
-
|
|
218
|
+
Assert.areEqual(1, logs.size(), 'Event handler should create log record');
|
|
218
219
|
}
|
|
219
220
|
```
|
|
@@ -1,113 +1,18 @@
|
|
|
1
1
|
# TestDataFactory Patterns
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
For the base class template, see [assets/test-data-factory-template.cls](../assets/test-data-factory-template.cls).
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Design Rules
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
// ============ ACCOUNTS ============
|
|
14
|
-
|
|
15
|
-
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
|
|
16
|
-
List<Account> accounts = new List<Account>();
|
|
17
|
-
for (Integer i = 0; i < count; i++) {
|
|
18
|
-
accounts.add(new Account(
|
|
19
|
-
Name = 'Test Account ' + i,
|
|
20
|
-
BillingStreet = '123 Test St',
|
|
21
|
-
BillingCity = 'San Francisco',
|
|
22
|
-
BillingState = 'CA',
|
|
23
|
-
BillingPostalCode = '94105',
|
|
24
|
-
BillingCountry = 'USA',
|
|
25
|
-
Industry = 'Technology',
|
|
26
|
-
Type = 'Customer'
|
|
27
|
-
));
|
|
28
|
-
}
|
|
29
|
-
if (doInsert) insert accounts;
|
|
30
|
-
return accounts;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
public static Account createAccount(Boolean doInsert) {
|
|
34
|
-
return createAccounts(1, doInsert)[0];
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// ============ CONTACTS ============
|
|
38
|
-
|
|
39
|
-
public static List<Contact> createContacts(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
|
|
40
|
-
List<Contact> contacts = new List<Contact>();
|
|
41
|
-
Integer index = 0;
|
|
42
|
-
for (Account acc : accounts) {
|
|
43
|
-
for (Integer i = 0; i < countPerAccount; i++) {
|
|
44
|
-
contacts.add(new Contact(
|
|
45
|
-
FirstName = 'Test',
|
|
46
|
-
LastName = 'Contact ' + index,
|
|
47
|
-
Email = 'test.contact' + index + '@example.com',
|
|
48
|
-
Phone = '555-000-' + String.valueOf(index).leftPad(4, '0'),
|
|
49
|
-
AccountId = acc.Id
|
|
50
|
-
));
|
|
51
|
-
index++;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
if (doInsert) insert contacts;
|
|
55
|
-
return contacts;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ============ OPPORTUNITIES ============
|
|
59
|
-
|
|
60
|
-
public static List<Opportunity> createOpportunities(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
|
|
61
|
-
List<Opportunity> opps = new List<Opportunity>();
|
|
62
|
-
Integer index = 0;
|
|
63
|
-
for (Account acc : accounts) {
|
|
64
|
-
for (Integer i = 0; i < countPerAccount; i++) {
|
|
65
|
-
opps.add(new Opportunity(
|
|
66
|
-
Name = 'Test Opportunity ' + index,
|
|
67
|
-
AccountId = acc.Id,
|
|
68
|
-
StageName = 'Prospecting',
|
|
69
|
-
CloseDate = Date.today().addDays(30),
|
|
70
|
-
Amount = 10000 + (index * 1000)
|
|
71
|
-
));
|
|
72
|
-
index++;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
if (doInsert) insert opps;
|
|
76
|
-
return opps;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// ============ USERS ============
|
|
80
|
-
|
|
81
|
-
public static User createUser(String profileName, Boolean doInsert) {
|
|
82
|
-
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
|
|
83
|
-
String uniqueKey = String.valueOf(DateTime.now().getTime());
|
|
84
|
-
|
|
85
|
-
User u = new User(
|
|
86
|
-
FirstName = 'Test',
|
|
87
|
-
LastName = 'User ' + uniqueKey,
|
|
88
|
-
Email = 'testuser' + uniqueKey + '@example.com',
|
|
89
|
-
Username = 'testuser' + uniqueKey + '@example.com.test',
|
|
90
|
-
Alias = 'tuser',
|
|
91
|
-
TimeZoneSidKey = 'America/Los_Angeles',
|
|
92
|
-
LocaleSidKey = 'en_US',
|
|
93
|
-
EmailEncodingKey = 'UTF-8',
|
|
94
|
-
LanguageLocaleKey = 'en_US',
|
|
95
|
-
ProfileId = p.Id
|
|
96
|
-
);
|
|
97
|
-
if (doInsert) insert u;
|
|
98
|
-
return u;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// ============ CUSTOM OBJECTS ============
|
|
102
|
-
|
|
103
|
-
// Add methods for your custom objects following the same pattern:
|
|
104
|
-
// public static List<MyObject__c> createMyObjects(Integer count, Boolean doInsert) { ... }
|
|
105
|
-
}
|
|
106
|
-
```
|
|
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
|
|
107
12
|
|
|
108
13
|
## Field Override Pattern
|
|
109
14
|
|
|
110
|
-
Allow callers to override default values:
|
|
15
|
+
Allow callers to override default values without creating new factory methods:
|
|
111
16
|
|
|
112
17
|
```apex
|
|
113
18
|
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
|
|
@@ -115,12 +20,9 @@ public static Account createAccount(Map<String, Object> fieldOverrides, Boolean
|
|
|
115
20
|
Name = 'Test Account',
|
|
116
21
|
Industry = 'Technology'
|
|
117
22
|
);
|
|
118
|
-
|
|
119
|
-
// Apply overrides
|
|
120
23
|
for (String fieldName : fieldOverrides.keySet()) {
|
|
121
24
|
acc.put(fieldName, fieldOverrides.get(fieldName));
|
|
122
25
|
}
|
|
123
|
-
|
|
124
26
|
if (doInsert) insert acc;
|
|
125
27
|
return acc;
|
|
126
28
|
}
|
|
@@ -132,23 +34,6 @@ Account acc = TestDataFactory.createAccount(new Map<String, Object>{
|
|
|
132
34
|
}, true);
|
|
133
35
|
```
|
|
134
36
|
|
|
135
|
-
## Handling Required Fields and Validation Rules
|
|
136
|
-
|
|
137
|
-
```apex
|
|
138
|
-
public static Account createAccountWithRequiredFields(Boolean doInsert) {
|
|
139
|
-
Account acc = new Account(
|
|
140
|
-
Name = 'Test Account',
|
|
141
|
-
// Required custom fields
|
|
142
|
-
External_Id__c = 'EXT-' + String.valueOf(DateTime.now().getTime()),
|
|
143
|
-
// Fields required by validation rules
|
|
144
|
-
Phone = '555-123-4567',
|
|
145
|
-
Website = 'https://example.com'
|
|
146
|
-
);
|
|
147
|
-
if (doInsert) insert acc;
|
|
148
|
-
return acc;
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
37
|
## Record Type Support
|
|
153
38
|
|
|
154
39
|
```apex
|
|
@@ -157,7 +42,7 @@ public static Account createAccountByRecordType(String recordTypeName, Boolean d
|
|
|
157
42
|
.getRecordTypeInfosByDeveloperName()
|
|
158
43
|
.get(recordTypeName)
|
|
159
44
|
.getRecordTypeId();
|
|
160
|
-
|
|
45
|
+
|
|
161
46
|
Account acc = new Account(
|
|
162
47
|
Name = 'Test Account',
|
|
163
48
|
RecordTypeId = recordTypeId
|
|
@@ -167,10 +52,24 @@ public static Account createAccountByRecordType(String recordTypeName, Boolean d
|
|
|
167
52
|
}
|
|
168
53
|
```
|
|
169
54
|
|
|
170
|
-
##
|
|
55
|
+
## Handling Duplicate Rules
|
|
171
56
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
|
@@ -63,7 +63,7 @@ Use the default templates in the docs below. Values in `{braces}` are resolved p
|
|
|
63
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
64
|
|
|
65
65
|
### Step 4: Resolve Additional Configurations
|
|
66
|
-
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.
|
|
67
67
|
|
|
68
68
|
## Verification Checklist
|
|
69
69
|
Before deploying, confirm:
|
|
@@ -71,7 +71,7 @@ Before deploying, confirm:
|
|
|
71
71
|
- [ ] All five required properties are resolved
|
|
72
72
|
- [ ] All metadata directories and files exist per the project structure
|
|
73
73
|
- [ ] All metadata fields are populated per the templates and user requests
|
|
74
|
-
- [ ] `appSpace` in `content.json` matches an existing `
|
|
74
|
+
- [ ] `appSpace` in `content.json` matches an existing `UIBundle` metadata record
|
|
75
75
|
- [ ] Deployment validates successfully:
|
|
76
76
|
```bash
|
|
77
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.
|
|
@@ -43,20 +43,36 @@ sf template generate flexipage \
|
|
|
43
43
|
--output-dir force-app/main/default/flexipages
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
**
|
|
47
|
-
```bash
|
|
48
|
-
npm install -g @salesforce/cli@latest
|
|
49
|
-
```
|
|
46
|
+
**CRITICAL:** If the `sf template generate flexipage` command fails, **STOP**.
|
|
50
47
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
48
|
+
1. Install the templates plugin:
|
|
49
|
+
```bash
|
|
50
|
+
sf plugins install templates
|
|
51
|
+
```
|
|
52
|
+
2. Retry the `sf template generate flexipage` command
|
|
53
|
+
3. Verify the FlexiPage XML file was created
|
|
54
|
+
|
|
55
|
+
Do NOT continue to Step 2 until the template command succeeds. The generated XML is required for the entire workflow.
|
|
54
56
|
|
|
55
57
|
#### **Template-specific requirements**
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
-
|
|
59
|
-
-
|
|
58
|
+
|
|
59
|
+
**RecordPage:**
|
|
60
|
+
- Requires `--sobject` (e.g., Account, Custom_Object__c)
|
|
61
|
+
- Requires field parameters:
|
|
62
|
+
- `--primary-field`: Most important identifying field (e.g., Name)
|
|
63
|
+
- `--secondary-fields`: Record summary (recommended 4-6, max 12)
|
|
64
|
+
- `--detail-fields`: Full record details, including required fields (e.g., Name)
|
|
65
|
+
|
|
66
|
+
**AppPage:**
|
|
67
|
+
- No additional requirements
|
|
68
|
+
|
|
69
|
+
**HomePage:**
|
|
70
|
+
- No additional requirements
|
|
71
|
+
|
|
72
|
+
#### **Field Selection Rules**
|
|
73
|
+
- **Validate fields exist**: Use MCP tools or describe commands to discover available fields for the object before specifying them in the command
|
|
74
|
+
- **Prefer compound fields**: Use `Name` (not `FirstName`/`LastName`), `BillingAddress` (not `BillingStreet`/`BillingCity`/`BillingState`), `MailingAddress`, etc. when available
|
|
75
|
+
- **Include required fields in detail-fields**: Always include object required fields (like `Name`) in the `--detail-fields` parameter, even if they're also used in `--primary-field` or `--secondary-fields`
|
|
60
76
|
|
|
61
77
|
#### **What you get**
|
|
62
78
|
- Valid FlexiPage XML with correct structure
|
|
@@ -238,7 +254,7 @@ Every fieldInstance requires:
|
|
|
238
254
|
|
|
239
255
|
### "We couldn't retrieve or load the information on the field"
|
|
240
256
|
**Cause:** Invalid field API name - field doesn't exist on the object or has incorrect spelling
|
|
241
|
-
**Fix:** Use MCP tools or describe commands to discover valid fields, then update the field reference (see Field Selection
|
|
257
|
+
**Fix:** Use MCP tools or describe commands to discover valid fields, then update the field reference (see Field Selection Rules)
|
|
242
258
|
|
|
243
259
|
### "Invalid field reference"
|
|
244
260
|
**Cause:** Used `ObjectName.Field` instead of `Record.Field`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generating-ui-bundle-features
|
|
3
|
+
description: "Search and install pre-built features into Salesforce React UI bundles — authentication, shadcn, search, navigation, GraphQL, Agentforce AI, and more. Use whenever searching for or installing features. Always check for an existing feature before building from scratch. Triggers on: install feature, add authentication, add shadcn, add feature, search features, list features."
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# UI Bundle Features
|
|
7
|
+
|
|
8
|
+
## Installing Pre-built Features
|
|
9
|
+
|
|
10
|
+
Always check for an existing feature before building something from scratch. The features CLI installs pre-built, tested packages into Salesforce UI bundles — from foundational UI libraries (shadcn/ui) to full-stack capabilities (authentication, search, navigation, GraphQL, Agentforce AI).
|
|
11
|
+
|
|
12
|
+
### Workflow
|
|
13
|
+
|
|
14
|
+
1. **Search project code first** — check `src/` for existing implementations before installing anything. Scope searches to `src/` to avoid matching `node_modules/` or `dist/`.
|
|
15
|
+
|
|
16
|
+
2. **Search available features** — use `npx @salesforce/ui-bundle-features list` with `--search <query>` to filter by keyword. Use `--verbose` for full descriptions.
|
|
17
|
+
|
|
18
|
+
3. **Describe a feature** — use `npx @salesforce/ui-bundle-features describe <feature>` to see components, dependencies, copy operations, and example files.
|
|
19
|
+
|
|
20
|
+
4. **Install** — use `npx @salesforce/ui-bundle-features install <feature> --ui-bundle-dir <name>`. Key options:
|
|
21
|
+
- `--dry-run` to preview changes
|
|
22
|
+
- `--yes` for non-interactive mode (skips conflicts)
|
|
23
|
+
- `--on-conflict error` to detect conflicts, then `--conflict-resolution <file>` to resolve them
|
|
24
|
+
|
|
25
|
+
If no matching feature is found, ask the user before building a custom implementation — a relevant feature may exist under a different name.
|
|
26
|
+
|
|
27
|
+
### Conflict Handling
|
|
28
|
+
|
|
29
|
+
In non-interactive environments, use the two-pass approach: first run with `--on-conflict error` to detect conflicts, then create a resolution JSON file (`{ "path": "skip" | "overwrite" }`) and re-run with `--conflict-resolution`.
|
|
30
|
+
|
|
31
|
+
### Post-install: Integrating Example Files
|
|
32
|
+
|
|
33
|
+
Features may include `__example__` files showing integration patterns. For each:
|
|
34
|
+
|
|
35
|
+
1. Read the example file to understand the pattern
|
|
36
|
+
2. Read the target file (shown in `describe` output)
|
|
37
|
+
3. Apply the pattern from the example into the target
|
|
38
|
+
4. Delete the example file after successful integration
|
|
39
|
+
|
|
40
|
+
### Hint Placeholders
|
|
41
|
+
|
|
42
|
+
Some copy paths use `<descriptive-name>` placeholders (e.g., `<desired-page-with-search-input>`) that the CLI does not resolve. After installation, rename or relocate these files to the intended target, or integrate their patterns into an existing file.
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|