diff --git a/TODO.txt b/TODO.txt
index 39df698f992..822db33c41f 100644
--- a/TODO.txt
+++ b/TODO.txt
@@ -35,7 +35,9 @@ Look at:
* Once you add it, it cannot be removed
* Always qualify the record page target with the sobject types it's appropriate for
-
+ * use standard validation techniques
+ * Add the property data-validatable
+ * call reportValidity, binding it to the LWC
* To finalise the core architecture:
* Do we need to have a non all-or-nothing version of commitWork?
diff --git a/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml b/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml
index f10c5f7982b..d42177860d4 100644
--- a/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml
+++ b/framework/default/ortoo-core/default/labels/ortoo-core-CustomLabels.labels-meta.xml
@@ -105,6 +105,13 @@
The word 'Save', capitalised.
Save
+
+ ortoo_core_edit
+ en_US
+ false
+ The word 'Edit', capitalised.
+ Edit
+
ortoo_core_error_title
en_US
diff --git a/framework/default/ortoo-core/default/lwc/viewAndEditForm/__tests__/viewAndEditForm.test.js b/framework/default/ortoo-core/default/lwc/viewAndEditForm/__tests__/viewAndEditForm.test.js
new file mode 100644
index 00000000000..c66360dda2c
--- /dev/null
+++ b/framework/default/ortoo-core/default/lwc/viewAndEditForm/__tests__/viewAndEditForm.test.js
@@ -0,0 +1,73 @@
+import { createElement } from 'lwc';
+import ViewAndEditForm from 'c/viewAndEditForm';
+
+describe('c-view-and-edit-form', () => {
+ afterEach(() => {
+ // The jsdom instance is shared across test cases in a single file so reset the DOM
+ while (document.body.firstChild) {
+ document.body.removeChild(document.body.firstChild);
+ }
+ });
+
+ it('When inEditMode, has a save and cancel button, but no edit', () => {
+ const element = createElement('c-view-and-edit-form', {
+ is: ViewAndEditForm
+ });
+ element.inEditMode = true;
+ document.body.appendChild(element);
+
+ const saveButton = element.shadowRoot.querySelector( '[data-name="save"]' );
+ expect( saveButton ).not.toBe( null );
+
+ const cancelButton = element.shadowRoot.querySelector( '[data-name="cancel"]' );
+ expect( cancelButton ).not.toBe( null );
+
+ const editButton = element.shadowRoot.querySelector( '[data-name="edit"]' );
+ expect( editButton ).toBe( null );
+ });
+
+ it('When not inEditMode, has an edit button, but no save or cancel', () => {
+ const element = createElement('c-view-and-edit-form', {
+ is: ViewAndEditForm
+ });
+ element.inEditMode = false;
+ document.body.appendChild(element);
+
+ const saveButton = element.shadowRoot.querySelector( '[data-name="save"]' );
+ expect( saveButton ).toBe( null );
+
+ const cancelButton = element.shadowRoot.querySelector( '[data-name="cancel"]' );
+ expect( cancelButton ).toBe( null );
+
+ const editButton = element.shadowRoot.querySelector( '[data-name="edit"]' );
+ expect( editButton ).not.toBe( null );
+ });
+
+ it('When inEditMode, has an editForm slot but no viewForm slot', () => {
+ const element = createElement('c-view-and-edit-form', {
+ is: ViewAndEditForm
+ });
+ element.inEditMode = true;
+ document.body.appendChild(element);
+
+ const viewForm = element.shadowRoot.querySelector( 'slot[name="viewForm"]' );
+ expect( viewForm ).toBe( null );
+
+ const editForm = element.shadowRoot.querySelector( 'slot[name="editForm"]' );
+ expect( editForm ).not.toBe( null );
+ });
+
+ it('When not inEditMode, has a viewForm slot but no editForm slot', () => {
+ const element = createElement('c-view-and-edit-form', {
+ is: ViewAndEditForm
+ });
+ element.inEditMode = false;
+ document.body.appendChild(element);
+
+ const viewForm = element.shadowRoot.querySelector( 'slot[name="viewForm"]' );
+ expect( viewForm ).not.toBe( null );
+
+ const editForm = element.shadowRoot.querySelector( 'slot[name="editForm"]' );
+ expect( editForm ).toBe( null );
+ });
+});
\ No newline at end of file
diff --git a/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.html b/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.html
index c1d37a7f115..a64dd72f1e7 100644
--- a/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.html
+++ b/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.html
@@ -13,6 +13,7 @@
class="slds-m-top_small slds-float_right"
variant="default"
name="edit"
+ data-name="edit"
label={editButtonLabel}
onclick={handleEditClick}
>
@@ -43,6 +44,7 @@
@@ -52,6 +54,7 @@
diff --git a/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.js b/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.js
index 0e1a1e10f01..b683a1c9590 100644
--- a/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.js
+++ b/framework/default/ortoo-core/default/lwc/viewAndEditForm/viewAndEditForm.js
@@ -1,13 +1,19 @@
import { LightningElement, api } from 'lwc';
+import SAVE_LABEL from '@salesforce/label/c.ortoo_core_save';
+import CANCEL_LABEL from '@salesforce/label/c.ortoo_core_cancel';
+import EDIT_LABEL from '@salesforce/label/c.ortoo_core_edit';
+
+/**
+ * Provides a standard format for a dual mode, view and edit form, including the rendering of edit / save and cancel buttons
+ */
export default class ViewAndEditForm extends LightningElement {
@api inEditMode = false;
- // TODO: labels
- @api editButtonLabel = 'Edit';
- @api cancelButtonLabel = 'Cancel';
- @api saveButtonLabel = 'Save';
+ @api editButtonLabel = EDIT_LABEL;
+ @api cancelButtonLabel = CANCEL_LABEL;
+ @api saveButtonLabel = SAVE_LABEL;
@api displayDensity;