diff --git a/build/nightly-E2E-test-pipelines.yml b/build/nightly-E2E-test-pipelines.yml
index 7bbdec8e145c..e95d6ac403c8 100644
--- a/build/nightly-E2E-test-pipelines.yml
+++ b/build/nightly-E2E-test-pipelines.yml
@@ -526,6 +526,23 @@ stages:
CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
additionalEnvironmentVariables: true
+ # ExtensionRegistry
+ WindowsExtensionRegistry:
+ vmImage: "windows-latest"
+ testFolder: "ExtensionRegistry"
+ port: ''
+ testCommand: "npx playwright test --project=extensionRegistry"
+ CONNECTIONSTRINGS__UMBRACODBDSN: Data Source=(localdb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Umbraco.mdf;Integrated Security=True
+ CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
+ additionalEnvironmentVariables: false
+ LinuxExtensionRegistry:
+ vmImage: "ubuntu-latest"
+ testFolder: "ExtensionRegistry"
+ port: ''
+ testCommand: "npx playwright test --project=extensionRegistry"
+ CONNECTIONSTRINGS__UMBRACODBDSN: Server=(local);Database=Umbraco;User Id=sa;Password=$(SA_PASSWORD);Encrypt=True;TrustServerCertificate=True
+ CONNECTIONSTRINGS__UMBRACODBDSN_PROVIDERNAME: Microsoft.Data.SqlClient
+ additionalEnvironmentVariables: false
pool:
vmImage: $(vmImage)
steps:
@@ -657,4 +674,4 @@ stages:
--data "$PAYLOAD" \
"$SLACK_WEBHOOK_URL"
env:
- SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
+ SLACK_WEBHOOK_URL: $(E2ESLACKWEBHOOKURL)
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts
index c09d132505c8..72776856bdeb 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/playwright.config.ts
@@ -54,6 +54,17 @@ export default defineConfig({
storageState: STORAGE_STATE
}
},
+ {
+ name: 'extensionRegistry',
+ testMatch: 'ExtensionRegistry/*.spec.ts',
+ dependencies: ['setup'],
+ use: {
+ ...devices['Desktop Chrome'],
+ // Use prepared auth state.
+ ignoreHTTPSErrors: true,
+ storageState: STORAGE_STATE
+ }
+ },
{
name: 'deliveryApi',
testMatch: 'DeliveryApi/**',
@@ -63,7 +74,7 @@ export default defineConfig({
// Use prepared auth state.
ignoreHTTPSErrors: true,
storageState: STORAGE_STATE
- },
+ }
},
{
name: 'externalLoginAzureADB2C',
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/DeliveryApi/DeliveryApi.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/DeliveryApi/DeliveryApi.spec.ts
index 5d3ae153ebc1..cde8e2c9ccd2 100644
--- a/tests/Umbraco.Tests.AcceptanceTest/tests/DeliveryApi/DeliveryApi.spec.ts
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/DeliveryApi/DeliveryApi.spec.ts
@@ -1,7 +1,7 @@
import {expect} from '@playwright/test';
import {AliasHelper, test} from '@umbraco/playwright-testhelpers';
-test('can get content from delivery api', async ({umbracoApi}) => {
+test.skip('can get content from delivery api', async ({umbracoApi}) => {
// Arrange
const documentTypeName = 'TestDocumentType';
const contentName = 'TestContent';
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/dist/customtexteditor.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/dist/customtexteditor.js
new file mode 100644
index 000000000000..7081fa59d1bf
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/dist/customtexteditor.js
@@ -0,0 +1,100 @@
+import { css as p, property as u, state as c, customElement as g, html as l } from "@umbraco-cms/backoffice/external/lit";
+import { UmbLitElement as m } from "@umbraco-cms/backoffice/lit-element";
+import { UmbTextStyles as v } from "@umbraco-cms/backoffice/style";
+var x = Object.defineProperty, _ = Object.getOwnPropertyDescriptor, o = (t, e, a, n) => {
+ for (var r = n > 1 ? void 0 : n ? _(e, a) : e, i = t.length - 1, h; i >= 0; i--)
+ (h = t[i]) && (r = (n ? h(e, a, r) : h(r)) || r);
+ return n && r && x(e, a, r), r;
+};
+let s = class extends m {
+ constructor() {
+ super(...arguments), this.value = "";
+ }
+ render() {
+ const t = this.value?.length || 0, a = this._maxLength !== null && this._maxLength !== void 0 && this._maxLength > 0;
+ let n = "";
+ return this._maxLength && (t > this._maxLength * 0.9 ? n = "danger" : t > this._maxLength * 0.7 && (n = "warning")), l`
+
+ ${a ? l`
+
+ ${t}/${this._maxLength}
+
+ ` : ""}
+ `;
+ }
+ connectedCallback() {
+ super.connectedCallback(), this._updateConfigValues();
+ }
+ _updateConfigValues() {
+ this._maxLength = this.config?.getValueByAlias("maxChars"), this._placeholder = this.config?.getValueByAlias("placeholder") || "Enter text here...";
+ }
+ _onInput(t) {
+ const e = t.target, a = e.value;
+ if (this._maxLength && a.length > this._maxLength) {
+ e.value = this.value;
+ return;
+ }
+ this.value = a, this._dispatchChangeEvent();
+ }
+ _dispatchChangeEvent() {
+ this.dispatchEvent(
+ new CustomEvent("property-value-change", {
+ detail: {
+ value: this.value
+ },
+ bubbles: !0,
+ composed: !0
+ })
+ );
+ }
+};
+s.styles = [
+ v,
+ p`
+ .text-input{
+ width: 100%;
+ }
+ .char-counter {
+ position: absolute;
+ bottom: -20px;
+ right: 0;
+ font-size: 12px;
+ color: var(--uui-color-text-alt);
+ }
+
+ .char-counter.warning {
+ color: var(--uui-color-warning);
+ }
+
+ .char-counter.danger {
+ color: var(--uui-color-danger);
+ }
+ `
+];
+o([
+ u({ type: String })
+], s.prototype, "value", 2);
+o([
+ u({ attribute: !1 })
+], s.prototype, "config", 2);
+o([
+ c()
+], s.prototype, "_maxLength", 2);
+o([
+ c()
+], s.prototype, "_placeholder", 2);
+s = o([
+ g("custom-text-editor")
+], s);
+export {
+ s as CustomTextEditorElement,
+ s as default
+};
+//# sourceMappingURL=customtexteditor.js.map
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/dist/customtexteditor.js.map b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/dist/customtexteditor.js.map
new file mode 100644
index 000000000000..14c2bd2d0eb9
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/dist/customtexteditor.js.map
@@ -0,0 +1 @@
+{"version":3,"file":"customtexteditor.js","sources":["../src/customtexteditor.ts"],"sourcesContent":["import { html, css, customElement, property, state } from '@umbraco-cms/backoffice/external/lit';\r\nimport { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';\r\nimport type { UmbPropertyEditorConfigCollection, UmbPropertyEditorUiElement } from '@umbraco-cms/backoffice/property-editor';\r\nimport { UmbTextStyles } from '@umbraco-cms/backoffice/style';\r\n\r\n@customElement('custom-text-editor')\r\nexport class CustomTextEditorElement extends UmbLitElement implements UmbPropertyEditorUiElement {\r\n\r\n @property({ type: String })\r\n value: string = '';\r\n\r\n @property({ attribute: false })\r\n public config?: UmbPropertyEditorConfigCollection;\r\n\r\n @state()\r\n private _maxLength?: number;\r\n\r\n @state()\r\n private _placeholder?: string;\r\n\r\n override render() {\r\n const characterCount = this.value?.length || 0;\r\n const hasMaxLength = this._maxLength !== null && this._maxLength !== undefined && this._maxLength > 0;\r\n const showCharCounter = hasMaxLength;\r\n\r\n // Safe calculation with null checks\r\n let counterClass = '';\r\n if (this._maxLength) {\r\n if (characterCount > this._maxLength * 0.9) {\r\n counterClass = 'danger';\r\n } else if (characterCount > this._maxLength * 0.7) {\r\n counterClass = 'warning';\r\n }\r\n }\r\n\r\n return html`\r\n \r\n ${showCharCounter\r\n ? html`\r\n \r\n ${characterCount}/${this._maxLength}\r\n
\r\n `\r\n : ''\r\n }\r\n `;\r\n }\r\n\r\n connectedCallback(): void {\r\n super.connectedCallback();\r\n this._updateConfigValues();\r\n }\r\n\r\n private _updateConfigValues(): void {\r\n this._maxLength = this.config?.getValueByAlias('maxChars') as number;\r\n this._placeholder = this.config?.getValueByAlias('placeholder') as string || 'Enter text here...';\r\n }\r\n\r\n private _onInput(event: Event): void {\r\n const target = event.target as HTMLInputElement;\r\n const newValue = target.value;\r\n\r\n // Apply max length validation if configured\r\n if (this._maxLength && newValue.length > this._maxLength) {\r\n target.value = this.value; // Revert to previous value\r\n return;\r\n }\r\n\r\n this.value = newValue;\r\n this._dispatchChangeEvent();\r\n }\r\n\r\n private _dispatchChangeEvent(): void {\r\n this.dispatchEvent(\r\n new CustomEvent('property-value-change', {\r\n detail: {\r\n value: this.value,\r\n },\r\n bubbles: true,\r\n composed: true,\r\n })\r\n );\r\n }\r\n\r\n static styles = [\r\n UmbTextStyles,\r\n css`\r\n .text-input{\r\n width: 100%;\r\n }\r\n .char-counter {\r\n position: absolute;\r\n bottom: -20px;\r\n right: 0;\r\n font-size: 12px;\r\n color: var(--uui-color-text-alt);\r\n }\r\n\r\n .char-counter.warning {\r\n color: var(--uui-color-warning);\r\n }\r\n\r\n .char-counter.danger {\r\n color: var(--uui-color-danger);\r\n }\r\n `];\r\n}\r\n\r\nexport {\r\n CustomTextEditorElement as default\r\n};"],"names":["CustomTextEditorElement","UmbLitElement","characterCount","showCharCounter","counterClass","html","event","target","newValue","UmbTextStyles","css","__decorateClass","property","state","customElement"],"mappings":";;;;;;;;AAMO,IAAMA,IAAN,cAAsCC,EAAoD;AAAA,EAA1F,cAAA;AAAA,UAAA,GAAA,SAAA,GAGH,KAAA,QAAgB;AAAA,EAAA;AAAA,EAWP,SAAS;AACd,UAAMC,IAAiB,KAAK,OAAO,UAAU,GAEvCC,IADe,KAAK,eAAe,QAAQ,KAAK,eAAe,UAAa,KAAK,aAAa;AAIpG,QAAIC,IAAe;AACnB,WAAI,KAAK,eACDF,IAAiB,KAAK,aAAa,MACnCE,IAAe,WACRF,IAAiB,KAAK,aAAa,QAC1CE,IAAe,aAIhBC;AAAA;AAAA;AAAA;AAAA,2BAIY,KAAK,SAAS,EAAE;AAAA,iCACV,KAAK,gBAAgB,EAAE;AAAA,+BACzB,KAAK,cAAc,EAAE;AAAA,2BACzB,KAAK,QAAQ;AAAA;AAAA,kBAEtBF,IACAE;AAAA,+CAC6BD,CAAY;AAAA,wBACnCF,CAAc,IAAI,KAAK,UAAU;AAAA;AAAA,sBAGvC,EACE;AAAA;AAAA,EAEhB;AAAA,EAEA,oBAA0B;AACtB,UAAM,kBAAA,GACN,KAAK,oBAAA;AAAA,EACT;AAAA,EAEQ,sBAA4B;AAChC,SAAK,aAAa,KAAK,QAAQ,gBAAgB,UAAU,GACzD,KAAK,eAAe,KAAK,QAAQ,gBAAgB,aAAa,KAAe;AAAA,EACjF;AAAA,EAEQ,SAASI,GAAoB;AACjC,UAAMC,IAASD,EAAM,QACfE,IAAWD,EAAO;AAGxB,QAAI,KAAK,cAAcC,EAAS,SAAS,KAAK,YAAY;AACtD,MAAAD,EAAO,QAAQ,KAAK;AACpB;AAAA,IACJ;AAEA,SAAK,QAAQC,GACb,KAAK,qBAAA;AAAA,EACT;AAAA,EAEQ,uBAA6B;AACjC,SAAK;AAAA,MACD,IAAI,YAAY,yBAAyB;AAAA,QACrC,QAAQ;AAAA,UACJ,OAAO,KAAK;AAAA,QAAA;AAAA,QAEhB,SAAS;AAAA,QACT,UAAU;AAAA,MAAA,CACb;AAAA,IAAA;AAAA,EAET;AAwBJ;AA3GaR,EAqFF,SAAS;AAAA,EACZS;AAAA,EACAC;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBC;AAvGLC,EAAA;AAAA,EADCC,EAAS,EAAE,MAAM,OAAA,CAAQ;AAAA,GAFjBZ,EAGT,WAAA,SAAA,CAAA;AAGOW,EAAA;AAAA,EADNC,EAAS,EAAE,WAAW,GAAA,CAAO;AAAA,GALrBZ,EAMF,WAAA,UAAA,CAAA;AAGCW,EAAA;AAAA,EADPE,EAAA;AAAM,GAREb,EASD,WAAA,cAAA,CAAA;AAGAW,EAAA;AAAA,EADPE,EAAA;AAAM,GAXEb,EAYD,WAAA,gBAAA,CAAA;AAZCA,IAANW,EAAA;AAAA,EADNG,EAAc,oBAAoB;AAAA,GACtBd,CAAA;"}
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/umbraco-package.json
new file mode 100644
index 000000000000..bda5dc316f69
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/custom-property-editor/umbraco-package.json
@@ -0,0 +1,18 @@
+{
+ "$schema": "../../umbraco-package-schema.json",
+ "name": "My.Extension",
+ "extensions": [
+ {
+ "type": "propertyEditorUi",
+ "alias": "Custom.TextEditor",
+ "name": "Custom Text Editor",
+ "element": "/App_Plugins/custom-property-editor/dist/customtexteditor.js",
+ "meta": {
+ "label": "Custom Text Editor",
+ "propertyEditorSchemaAlias": "Umbraco.TextBox",
+ "icon": "icon-edit",
+ "group": "Custom"
+ }
+ }
+ ]
+}
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/lock-action.api.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/lock-action.api.js
new file mode 100644
index 000000000000..639ab422b9d0
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/lock-action.api.js
@@ -0,0 +1,25 @@
+import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from '@umbraco-cms/backoffice/document';
+import { UmbVariantId } from '@umbraco-cms/backoffice/variant';
+import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
+
+export class UmbDocumentLockEntityAction extends UmbEntityActionBase {
+ async execute() {
+ const context = await this.getContext(UMB_DOCUMENT_WORKSPACE_CONTEXT);
+ const ruleUnique = 'lock';
+
+ const myRule = {
+ unique: ruleUnique,
+ UmbVariantId: new UmbVariantId(),
+ };
+
+ const hasRule = context?.readOnlyGuard.getRules().find((rule) => rule.unique === ruleUnique);
+
+ if (hasRule) {
+ context?.readOnlyGuard.removeRule(ruleUnique);
+ } else {
+ context?.readOnlyGuard.addRule(myRule);
+ }
+ }
+}
+
+export { UmbDocumentLockEntityAction as api };
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/lock-action.manifests.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/lock-action.manifests.js
new file mode 100644
index 000000000000..ef8683502ecd
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/lock-action.manifests.js
@@ -0,0 +1,17 @@
+import { UMB_DOCUMENT_ENTITY_TYPE } from '@umbraco-cms/backoffice/document';
+
+export const manifests = [
+ {
+ type: 'entityAction',
+ kind: 'default',
+ alias: 'Umb.EntityAction.Document.Lock',
+ name: 'Lock Document Entity Action',
+ forEntityTypes: [UMB_DOCUMENT_ENTITY_TYPE],
+ api: () => import('./lock-action.api.js'),
+ weight: 200,
+ meta: {
+ label: 'Lock it',
+ icon: 'icon-lock',
+ }
+ }
+]
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/umbraco-package.json
new file mode 100644
index 000000000000..eb46049cfe34
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/lock-action/umbraco-package.json
@@ -0,0 +1,12 @@
+ {
+ "name": "E2E Test Package",
+ "version": "1.0.0",
+ "extensions": [
+ {
+ "type": "bundle",
+ "alias": "E2E.Package.Bundle",
+ "name": "E2E Test Package Bundle",
+ "js": "/App_Plugins/lock-action/lock-action.manifests.js"
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/retrieve-action.api.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/retrieve-action.api.js
new file mode 100644
index 000000000000..cd16d528c540
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/retrieve-action.api.js
@@ -0,0 +1,18 @@
+import { UmbEntityActionBase } from '@umbraco-cms/backoffice/entity-action';
+import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
+
+export class RetrieveAction extends UmbEntityActionBase {
+
+ async execute() {
+ const { entityType, unique } = this.args;
+ const message = `${entityType}_${unique}`;
+ const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
+ notificationContext?.peek('positive', {
+ data: {
+ headline: '',
+ message: message,
+ },
+ });
+ }
+}
+export { RetrieveAction as api };
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/retrieve-action.manifests.js b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/retrieve-action.manifests.js
new file mode 100644
index 000000000000..34062853b3df
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/retrieve-action.manifests.js
@@ -0,0 +1,13 @@
+export const manifest = {
+ type: 'entityAction',
+ kind: 'default',
+ alias: 'Retrieve',
+ name: 'Retrieve',
+ weight: 200,
+ forEntityTypes: ['document', 'media'],
+ api: () => import('./retrieve-action.api.js'),
+ meta: {
+ icon: 'icon-add',
+ label: 'Retrieve'
+ },
+};
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/umbraco-package.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/umbraco-package.json
new file mode 100644
index 000000000000..8f6b011869ca
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/App_Plugins/retrieve-action/umbraco-package.json
@@ -0,0 +1,12 @@
+{
+ "name": "My entity action",
+ "version": "1.0.0",
+ "extensions": [
+ {
+ "type": "bundle",
+ "alias": "Umb.EntityAction.Test.Bundle",
+ "name": "Test entity action bundle",
+ "js": "/App_Plugins/retrieve-action/retrieve-action.manifests.js"
+ }
+ ]
+}
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/appsettings.json b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/appsettings.json
new file mode 100644
index 000000000000..49d90bb5936e
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/AdditionalSetup/appsettings.json
@@ -0,0 +1,58 @@
+{
+ "$schema": "appsettings-schema.json",
+ "Serilog": {
+ "MinimumLevel": {
+ "Default": "Information",
+ "Override": {
+ "Microsoft": "Warning",
+ "Microsoft.Hosting.Lifetime": "Information",
+ "System": "Warning"
+ }
+ },
+ "WriteTo": [
+ {
+ "Name": "Async",
+ "Args": {
+ "Configure": [
+ {
+ "Name": "Console"
+ }
+ ]
+ }
+ }
+ ]
+ },
+ "Umbraco": {
+ "CMS": {
+ "Unattended": {
+ "InstallUnattended": true,
+ "UnattendedUserName": "Playwright Test",
+ "UnattendedUserEmail": "playwright@umbraco.com",
+ "UnattendedUserPassword": "UmbracoAcceptance123!"
+ },
+ "Content": {
+ "ContentVersionCleanupPolicy": {
+ "EnableCleanup": false
+ }
+ },
+ "Global": {
+ "DisableElectionForSingleServer": true,
+ "InstallMissingDatabase": true,
+ "Id": "00000000-0000-0000-0000-000000000042",
+ "VersionCheckPeriod": 0,
+ "UseHttps": true
+ },
+ "HealthChecks": {
+ "Notification": {
+ "Enabled": false
+ }
+ },
+ "KeepAlive": {
+ "DisableKeepAliveTask": true
+ },
+ "WebRouting": {
+ "UmbracoApplicationUrl": "https://localhost:44331/"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomPropertyEditor.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomPropertyEditor.spec.ts
new file mode 100644
index 000000000000..98323e988a1b
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/CustomPropertyEditor.spec.ts
@@ -0,0 +1,87 @@
+import {expect} from '@playwright/test';
+import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
+
+// Content
+const contentName = 'TestContent';
+// DocumentType
+const documentTypeName = 'TestDocumentTypeForContent';
+// DataType
+const dataTypeName = 'CustomTextBox';
+const editorUiAlias = 'Custom.TextEditor';
+const editorAlias = 'Umbraco.TextBox';
+// Property Editor
+const customPropertyEditorName = 'Custom Text Editor';
+// Test values
+const testValue = 'This is a test value for the custom property editor';
+
+test.afterEach(async ({umbracoApi}) => {
+ await umbracoApi.document.ensureNameNotExists(contentName);
+ await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
+ await umbracoApi.dataType.ensureNameNotExists(dataTypeName);
+});
+
+test('can add custom property editor to a document type', async ({umbracoApi, umbracoUi}) => {
+ // Arrange
+ await umbracoUi.goToBackOffice();
+ await umbracoUi.dataType.goToSection(ConstantHelper.sections.settings);
+
+ // Act
+ await umbracoUi.dataType.clickActionsMenuAtRoot();
+ await umbracoUi.dataType.clickCreateActionMenuOption();
+ await umbracoUi.dataType.clickDataTypeButton();
+ await umbracoUi.dataType.enterDataTypeName(dataTypeName);
+ await umbracoUi.dataType.clickSelectAPropertyEditorButton();
+ await umbracoUi.dataType.selectAPropertyEditor(customPropertyEditorName);
+ await umbracoUi.dataType.clickSaveButton();
+
+ // Assert
+ await umbracoUi.dataType.waitForDataTypeToBeCreated();
+ await umbracoUi.dataType.isDataTypeTreeItemVisible(dataTypeName);
+ expect(await umbracoApi.dataType.doesNameExist(dataTypeName)).toBeTruthy();
+});
+
+test('can select custom property editor in property editor picker on data type', async ({umbracoApi, umbracoUi}) => {
+ // Arrange
+ await umbracoApi.documentType.createDefaultDocumentType(documentTypeName);
+ const dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, []);
+ await umbracoUi.goToBackOffice();
+ await umbracoUi.documentType.goToSection(ConstantHelper.sections.settings);
+
+ // Act
+ await umbracoUi.documentType.goToDocumentType(documentTypeName);
+ await umbracoUi.documentType.clickAddGroupButton();
+ await umbracoUi.documentType.addPropertyEditor(dataTypeName);
+ await umbracoUi.documentType.enterGroupName('Content');
+ await umbracoUi.documentType.clickSaveButton();
+
+ // Assert
+ await umbracoUi.documentType.waitForDocumentTypeToBeCreated();
+ expect(await umbracoApi.documentType.doesNameExist(documentTypeName)).toBeTruthy();
+ const documentTypeData = await umbracoApi.documentType.getByName(documentTypeName);
+ // Checks if the correct property was added to the document type
+ expect(documentTypeData.properties[0].dataType.id).toBe(dataTypeId);
+});
+
+test('can write and read value from custom property editor', async ({umbracoApi, umbracoUi}) => {
+ // Arrange
+ const dataTypeValue = [
+ {
+ alias: "maxChars",
+ value: "100",
+ },
+ ];
+ const dataTypeId = await umbracoApi.dataType.create(dataTypeName, editorAlias, editorUiAlias, dataTypeValue);
+ const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeId);
+ await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, "Test content", dataTypeName);
+ await umbracoUi.goToBackOffice();
+ await umbracoUi.content.goToSection(ConstantHelper.sections.content);
+
+ // Act
+ await umbracoUi.content.goToContentWithName(contentName);
+ await umbracoUi.content.enterPropertyValue(dataTypeName, testValue);
+ await umbracoUi.content.clickSaveButton();
+
+ // Assert
+ const contentData = await umbracoApi.document.getByName(contentName);
+ expect(contentData.values[0].value).toEqual(testValue);
+});
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/ReadOnlyGuardRules.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/ReadOnlyGuardRules.spec.ts
new file mode 100644
index 000000000000..a4f4500df05e
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/ReadOnlyGuardRules.spec.ts
@@ -0,0 +1,56 @@
+import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
+
+// Content
+const contentName = 'TestContent';
+const contentText = 'This is test content text';
+// DocumentType
+const documentTypeName = 'TestDocumentTypeForContent';
+// DataType
+const dataTypeName = 'Textstring';
+
+test.afterEach(async ({umbracoApi}) => {
+ await umbracoApi.document.ensureNameNotExists(contentName);
+ await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
+ await umbracoApi.language.ensureIsoCodeNotExists("da");
+});
+
+test('can lock an invariant content node', {tag: '@release'}, async ({umbracoApi, umbracoUi}) => {
+ // Arrange
+ const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
+ const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
+ await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, contentText, dataTypeName);
+ await umbracoUi.goToBackOffice();
+ await umbracoUi.content.goToSection(ConstantHelper.sections.content);
+
+ // Act
+ await umbracoUi.content.goToContentWithName(contentName);
+ await umbracoUi.content.isDocumentReadOnly(false);
+ await umbracoUi.content.clickWorkspaceActionMenuButton();
+ await umbracoUi.content.clickLockActionMenuOption();
+
+ // Assert
+ await umbracoUi.content.isContentNameReadOnly();
+ await umbracoUi.content.isDocumentReadOnly(true);
+ await umbracoUi.content.isPropertyEditorUiWithNameReadOnly("text-box");
+});
+
+test('can lock a variant content node', async ({umbracoApi, umbracoUi}) => {
+ // Arrange
+ await umbracoApi.language.createDanishLanguage();
+ const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
+ const documentTypeId = await umbracoApi.documentType.createVariantDocumentTypeWithInvariantPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
+ await umbracoApi.document.createDocumentWithEnglishCultureAndTextContent(contentName, documentTypeId, contentText, dataTypeName);
+ await umbracoUi.goToBackOffice();
+ await umbracoUi.content.goToSection(ConstantHelper.sections.content);
+
+ // Act
+ await umbracoUi.content.goToContentWithName(contentName);
+ await umbracoUi.content.isDocumentReadOnly(false);
+ await umbracoUi.content.clickWorkspaceActionMenuButton();
+ await umbracoUi.content.clickLockActionMenuOption();
+
+ // Assert
+ await umbracoUi.content.isContentNameReadOnly();
+ await umbracoUi.content.isDocumentReadOnly(true);
+ await umbracoUi.content.isPropertyEditorUiWithNameReadOnly("text-box");
+});
diff --git a/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/RetrieveAction.spec.ts b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/RetrieveAction.spec.ts
new file mode 100644
index 000000000000..155b0b39379e
--- /dev/null
+++ b/tests/Umbraco.Tests.AcceptanceTest/tests/ExtensionRegistry/RetrieveAction.spec.ts
@@ -0,0 +1,48 @@
+import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
+
+// Content
+const contentName = 'TestContent';
+// DocumentType
+const documentTypeName = 'TestDocumentTypeForContent';
+// DataType
+const dataTypeName = 'Textstring';
+//Media
+const mediaName = 'TestMedia';
+
+test.afterEach(async ({umbracoApi}) => {
+ await umbracoApi.document.ensureNameNotExists(contentName);
+ await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
+ await umbracoApi.media.ensureNameNotExists(mediaName);
+});
+
+test('can retrieve unique id and entity type of document', async ({umbracoApi, umbracoUi}) => {
+ // Arrange
+ const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
+ const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id);
+ const documentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, 'Test content', dataTypeName);
+ await umbracoUi.goToBackOffice();
+ await umbracoUi.content.goToSection(ConstantHelper.sections.content);
+
+ // Act
+ await umbracoUi.content.goToContentWithName(contentName);
+ await umbracoUi.content.clickWorkspaceActionMenuButton();
+ await umbracoUi.content.clickEntityActionWithName('Retrieve');
+
+ // Assert
+ await umbracoUi.content.doesSuccessNotificationHaveText('document_' + documentId);
+});
+
+test('can retrieve unique id and entity type of media', async ({umbracoApi, umbracoUi}) => {
+ // Arrange
+ const mediaId = await umbracoApi.media.createDefaultMediaWithImage(mediaName);
+ await umbracoUi.goToBackOffice();
+ await umbracoUi.content.goToSection(ConstantHelper.sections.media);
+
+ // Act
+ await umbracoUi.media.goToMediaWithName(mediaName);
+ await umbracoUi.media.clickWorkspaceActionMenuButton();
+ await umbracoUi.media.clickEntityActionWithName('Retrieve');
+
+ // Assert
+ await umbracoUi.media.doesSuccessNotificationHaveText('media_' + mediaId);
+});