Commit e3ae1106 authored by Benjamin Franzke's avatar Benjamin Franzke

[TASK] Migrate backend notifications to lit-html

Migrates backend UI component `Notification` to be based on `lit-html`.

Resolves: #93102
Releases: master
Change-Id: Ic79ff0261c75fa5e7654ff44c213784302dc1b59
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/67179Tested-by: default avatarTYPO3com <noreply@typo3.com>
Tested-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Tested-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
Reviewed-by: Benni Mack's avatarBenni Mack <benni@typo3.org>
Reviewed-by: Benjamin Franzke's avatarBenjamin Franzke <bfr@qbus.de>
parent bb94e866
......@@ -11,9 +11,9 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
// @todo Importing bootstrap here, to have jQuery.fn.alert applied
import 'bootstrap';
import {LitElement, html, customElement, property, internalProperty} from 'lit-element';
import {classMap} from 'lit-html/directives/class-map';
import {ifDefined} from 'lit-html/directives/if-defined';
import {AbstractAction} from './ActionButton/AbstractAction';
import {SeverityEnum} from './Enum/Severity';
import Severity = require('./Severity');
......@@ -29,7 +29,7 @@ interface Action {
*/
class Notification {
private static duration: number = 5;
private static messageContainer: JQuery = null;
private static messageContainer: HTMLElement = null;
/**
* Show a notice notification
......@@ -105,9 +105,81 @@ class Notification {
duration: number | string = this.duration,
actions: Array<Action> = [],
): void {
const className = Severity.getCssClass(severity);
duration = (typeof duration === 'undefined') ? this.duration : duration;
if (this.messageContainer === null || document.getElementById('alert-container') === null) {
this.messageContainer = document.createElement('div');
this.messageContainer.setAttribute('id', 'alert-container');
document.body.appendChild(this.messageContainer);
}
const box = <NotificationMessage>document.createElement('typo3-notification-message');
box.setAttribute('notificationId', 'notification-' + Math.random().toString(36).substr(2, 5));
box.setAttribute('title', title);
if (message) {
box.setAttribute('message', message);
}
box.setAttribute('severity', severity.toString());
box.setAttribute('duration', duration.toString());
box.actions = actions;
this.messageContainer.appendChild(box);
}
}
@customElement('typo3-notification-message')
class NotificationMessage extends LitElement {
@property() notificationId: string;
@property() title: string;
@property() message: string;
@property({type: Number}) severity: SeverityEnum = SeverityEnum.info;
@property() duration: number = 0;
@property({type: Array, attribute: false}) actions: Array<Action> = [];
@internalProperty() visible: boolean = false;
@internalProperty() executingAction: number = -1;
createRenderRoot(): Element|ShadowRoot {
return this;
}
async firstUpdated() {
await new Promise(resolve => window.setTimeout(resolve, 200));
this.visible = true;
await this.requestUpdate();
if (this.duration > 0) {
await new Promise(resolve => window.setTimeout(resolve, this.duration * 1000));
this.close();
}
}
async close(): Promise<void> {
this.visible = false;
const onfinish = () => {
this.parentNode && this.parentNode.removeChild(this);
};
if ('animate' in this) {
this.style.overflow = 'hidden';
this.style.display = 'block';
this.animate(
[
{ height: this.getBoundingClientRect().height + 'px' },
{ height: 0 },
], {
duration: 400,
easing: 'cubic-bezier(.02, .01, .47, 1)'
}
).onfinish = onfinish;
} else {
onfinish();
}
}
render() {
const className = Severity.getCssClass(this.severity);
let icon = '';
switch (severity) {
switch (this.severity) {
case SeverityEnum.notice:
icon = 'lightbulb-o';
break;
......@@ -125,102 +197,51 @@ class Notification {
icon = 'info';
}
duration = (typeof duration === 'undefined')
? this.duration
: (
typeof duration === 'string'
? parseFloat(duration)
: duration
);
if (this.messageContainer === null || document.getElementById('alert-container') === null) {
this.messageContainer = $('<div>', {'id': 'alert-container'}).appendTo('body');
}
const notificationId = 'notification-' + Math.random().toString(36).substr(2, 5);
const $box = $(
'<div id="' + notificationId + '" class="alert alert-' + className + ' alert-dismissible fade" role="alert">' +
'<button type="button" class="close" data-bs-dismiss="alert">' +
'<span aria-hidden="true"><i class="fa fa-times-circle"></i></span>' +
'<span class="sr-only">Close</span>' +
'</button>' +
'<div class="media">' +
'<div class="media-left">' +
'<span class="fa-stack fa-lg">' +
'<i class="fa fa-circle fa-stack-2x"></i>' +
'<i class="fa fa-' + icon + ' fa-stack-1x"></i>' +
'</span>' +
'</div>' +
'<div class="media-body">' +
'<h4 class="alert-title"></h4>' +
'<p class="alert-message text-pre-wrap"></p>' +
'</div>' +
'</div>' +
'<div class="alert-actions">' +
'</div>' +
'</div>',
);
$box.find('.alert-title').text(title);
$box.find('.alert-message').text(message);
const $actionButtonContainer = $box.find('.alert-actions');
if (actions.length > 0) {
for (let action of actions) {
const $actionButton = $('<a />', {
href: '#',
title: action.label,
});
$actionButton.text(action.label);
$actionButton.on('click', (e: JQueryEventObject): void => {
// Remove potentially set timeout
$box.clearQueue();
const target = <HTMLAnchorElement>e.currentTarget;
target.classList.add('executing');
$actionButtonContainer.find('a').not(target).addClass('disabled');
action.action.execute(target).then((): void => {
$box.alert('close');
});
});
$actionButtonContainer.append($actionButton);
}
} else {
$actionButtonContainer.remove();
}
$box.on('close.bs.alert', (e: Event) => {
e.preventDefault();
const $me = $(e.currentTarget);
$me
.clearQueue()
.queue((next: any): void => {
$me.removeClass('in');
next();
})
.slideUp({
complete: (): void => {
$me.remove();
},
});
});
$box.appendTo(this.messageContainer);
$box.delay(200)
.queue((next: any): void => {
$box.addClass('in');
next();
});
if (duration > 0) {
// if duration > 0 dismiss alert
$box.delay(duration * 1000)
.queue((next: any): void => {
$box.alert('close');
next();
});
}
/* eslint-disable @typescript-eslint/indent */
return html`
<div
id="${ifDefined(this.notificationId || undefined)}"
class="${'alert alert-' + className + ' alert-dismissible fade' + (this.visible ? ' in' : '')}"
role="alert">
<button type="button" class="close" @click="${async (e: Event) => this.close()}">
<span aria-hidden="true"><i class="fa fa-times-circle"></i></span>
<span class="sr-only">Close</span>
</button>
<div class="media">
<div class="media-left">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="${'fa fa-' + icon + ' fa-stack-1x'}"></i>
</span>
</div>
<div class="media-body">
<h4 class="alert-title">${this.title}</h4>
<p class="alert-message text-pre-wrap">${this.message ? this.message : ''}</p>
</div>
</div>
${this.actions.length === 0 ? '' : html`
<div class="alert-actions">
${this.actions.map((action, index) => html`
<a href="#"
title="${action.label}"
@click="${async (e: any) => {
e.preventDefault()
this.executingAction = index;
await this.updateComplete;
await action.action.execute(e.currentTarget);
this.close();
}}"
class="${classMap({
executing: this.executingAction === index,
disabled: this.executingAction >= 0 && this.executingAction !== index
})}"
>${action.label}</a>
`)}
</div>
`}
</div>
`;
/* eslint-enable @typescript-eslint/indent */
}
}
......
......@@ -11,26 +11,19 @@
* The TYPO3 project - inspiring people to share!
*/
import $ from 'jquery';
import DeferredAction = require('TYPO3/CMS/Backend/ActionButton/DeferredAction');
import ImmediateAction = require('TYPO3/CMS/Backend/ActionButton/ImmediateAction');
import Notification = require('TYPO3/CMS/Backend/Notification');
import type {LitElement} from 'lit-element';
describe('TYPO3/CMS/Backend/Notification:', () => {
beforeEach((): void => {
$.fx.off = true;
jasmine.clock().install();
const alertContainer = document.getElementById('alert-container');
while (alertContainer !== null && alertContainer.firstChild) {
alertContainer.removeChild(alertContainer.firstChild);
}
});
afterEach((): void => {
jasmine.clock().uninstall();
});
describe('can render notifications with dismiss after 1000ms', () => {
interface NotificationDataSet {
method: Function;
......@@ -75,21 +68,23 @@ describe('TYPO3/CMS/Backend/Notification:', () => {
}
for (let dataSet of notificationProvider()) {
it('can render a notification of type ' + dataSet.class, () => {
it('can render a notification of type ' + dataSet.class, async () => {
dataSet.method(dataSet.title, dataSet.message, 1);
await (document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement).updateComplete;
const alertSelector = 'div.alert.' + dataSet.class;
const alertBox = document.querySelector(alertSelector);
expect(alertBox).not.toBe(null);
expect(alertBox.querySelector('.alert-title').textContent).toEqual(dataSet.title);
expect(alertBox.querySelector('.alert-message').textContent).toEqual(dataSet.message);
jasmine.clock().tick(1200);
// wait for the notification to disappear for the next assertion (which tests for auto dismiss)
await new Promise(resolve => window.setTimeout(resolve, 2000));
expect(document.querySelector(alertSelector)).toBe(null);
});
}
});
it('can render action buttons', () => {
it('can render action buttons', async () => {
Notification.info(
'Info message',
'Some text',
......@@ -110,6 +105,7 @@ describe('TYPO3/CMS/Backend/Notification:', () => {
],
);
await (document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement).updateComplete;
const alertBox = document.querySelector('div.alert');
expect(alertBox.querySelector('.alert-actions')).not.toBe(null);
expect(alertBox.querySelectorAll('.alert-actions a').length).toEqual(2);
......@@ -117,7 +113,7 @@ describe('TYPO3/CMS/Backend/Notification:', () => {
expect(alertBox.querySelectorAll('.alert-actions a')[1].textContent).toEqual('My other action');
});
it('immediate action is called', () => {
it('immediate action is called', async () => {
const observer = {
callback: (): void => {
return;
......@@ -138,8 +134,10 @@ describe('TYPO3/CMS/Backend/Notification:', () => {
],
);
await (document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement).updateComplete;
const alertBox = document.querySelector('div.alert');
(<HTMLAnchorElement>alertBox.querySelector('.alert-actions a')).click();
await (document.querySelector('#alert-container typo3-notification-message:last-child') as LitElement).updateComplete;
expect(observer.callback).toHaveBeenCalled();
});
});
......@@ -10,4 +10,37 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","jquery","./Enum/Severity","./Severity","bootstrap"],(function(e,t,a,s,i){"use strict";a=__importDefault(a);class n{static notice(e,t,a,i){n.showMessage(e,t,s.SeverityEnum.notice,a,i)}static info(e,t,a,i){n.showMessage(e,t,s.SeverityEnum.info,a,i)}static success(e,t,a,i){n.showMessage(e,t,s.SeverityEnum.ok,a,i)}static warning(e,t,a,i){n.showMessage(e,t,s.SeverityEnum.warning,a,i)}static error(e,t,a=0,i){n.showMessage(e,t,s.SeverityEnum.error,a,i)}static showMessage(e,t,n=s.SeverityEnum.info,r=this.duration,o=[]){const l=i.getCssClass(n);let c="";switch(n){case s.SeverityEnum.notice:c="lightbulb-o";break;case s.SeverityEnum.ok:c="check";break;case s.SeverityEnum.warning:c="exclamation";break;case s.SeverityEnum.error:c="times";break;case s.SeverityEnum.info:default:c="info"}r=void 0===r?this.duration:"string"==typeof r?parseFloat(r):r,null!==this.messageContainer&&null!==document.getElementById("alert-container")||(this.messageContainer=a.default("<div>",{id:"alert-container"}).appendTo("body"));const d="notification-"+Math.random().toString(36).substr(2,5),u=a.default('<div id="'+d+'" class="alert alert-'+l+' alert-dismissible fade" role="alert"><button type="button" class="close" data-bs-dismiss="alert"><span aria-hidden="true"><i class="fa fa-times-circle"></i></span><span class="sr-only">Close</span></button><div class="media"><div class="media-left"><span class="fa-stack fa-lg"><i class="fa fa-circle fa-stack-2x"></i><i class="fa fa-'+c+' fa-stack-1x"></i></span></div><div class="media-body"><h4 class="alert-title"></h4><p class="alert-message text-pre-wrap"></p></div></div><div class="alert-actions"></div></div>');u.find(".alert-title").text(e),u.find(".alert-message").text(t);const f=u.find(".alert-actions");if(o.length>0)for(let e of o){const t=a.default("<a />",{href:"#",title:e.label});t.text(e.label),t.on("click",t=>{u.clearQueue();const a=t.currentTarget;a.classList.add("executing"),f.find("a").not(a).addClass("disabled"),e.action.execute(a).then(()=>{u.alert("close")})}),f.append(t)}else f.remove();u.on("close.bs.alert",e=>{e.preventDefault();const t=a.default(e.currentTarget);t.clearQueue().queue(e=>{t.removeClass("in"),e()}).slideUp({complete:()=>{t.remove()}})}),u.appendTo(this.messageContainer),u.delay(200).queue(e=>{u.addClass("in"),e()}),r>0&&u.delay(1e3*r).queue(e=>{u.alert("close"),e()})}}let r;n.duration=5,n.messageContainer=null;try{parent&&parent.window.TYPO3&&parent.window.TYPO3.Notification&&(r=parent.window.TYPO3.Notification),top&&top.TYPO3.Notification&&(r=top.TYPO3.Notification)}catch(e){}return r||(r=n,"undefined"!=typeof TYPO3&&(TYPO3.Notification=r)),r}));
\ No newline at end of file
var __decorate=this&&this.__decorate||function(t,e,i,s){var a,o=arguments.length,n=o<3?e:null===s?s=Object.getOwnPropertyDescriptor(e,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(t,e,i,s);else for(var r=t.length-1;r>=0;r--)(a=t[r])&&(n=(o<3?a(n):o>3?a(e,i,n):a(e,i))||n);return o>3&&n&&Object.defineProperty(e,i,n),n};define(["require","exports","lit-element","lit-html/directives/class-map","lit-html/directives/if-defined","./Enum/Severity","./Severity"],(function(t,e,i,s,a,o,n){"use strict";class r{static notice(t,e,i,s){r.showMessage(t,e,o.SeverityEnum.notice,i,s)}static info(t,e,i,s){r.showMessage(t,e,o.SeverityEnum.info,i,s)}static success(t,e,i,s){r.showMessage(t,e,o.SeverityEnum.ok,i,s)}static warning(t,e,i,s){r.showMessage(t,e,o.SeverityEnum.warning,i,s)}static error(t,e,i=0,s){r.showMessage(t,e,o.SeverityEnum.error,i,s)}static showMessage(t,e,i=o.SeverityEnum.info,s=this.duration,a=[]){s=void 0===s?this.duration:s,null!==this.messageContainer&&null!==document.getElementById("alert-container")||(this.messageContainer=document.createElement("div"),this.messageContainer.setAttribute("id","alert-container"),document.body.appendChild(this.messageContainer));const n=document.createElement("typo3-notification-message");n.setAttribute("notificationId","notification-"+Math.random().toString(36).substr(2,5)),n.setAttribute("title",t),e&&n.setAttribute("message",e),n.setAttribute("severity",i.toString()),n.setAttribute("duration",s.toString()),n.actions=a,this.messageContainer.appendChild(n)}}r.duration=5,r.messageContainer=null;let c,l=class extends i.LitElement{constructor(){super(...arguments),this.severity=o.SeverityEnum.info,this.duration=0,this.actions=[],this.visible=!1,this.executingAction=-1}createRenderRoot(){return this}async firstUpdated(){await new Promise(t=>window.setTimeout(t,200)),this.visible=!0,await this.requestUpdate(),this.duration>0&&(await new Promise(t=>window.setTimeout(t,1e3*this.duration)),this.close())}async close(){this.visible=!1;const t=()=>{this.parentNode&&this.parentNode.removeChild(this)};"animate"in this?(this.style.overflow="hidden",this.style.display="block",this.animate([{height:this.getBoundingClientRect().height+"px"},{height:0}],{duration:400,easing:"cubic-bezier(.02, .01, .47, 1)"}).onfinish=t):t()}render(){const t=n.getCssClass(this.severity);let e="";switch(this.severity){case o.SeverityEnum.notice:e="lightbulb-o";break;case o.SeverityEnum.ok:e="check";break;case o.SeverityEnum.warning:e="exclamation";break;case o.SeverityEnum.error:e="times";break;case o.SeverityEnum.info:default:e="info"}return i.html`
<div
id="${a.ifDefined(this.notificationId||void 0)}"
class="${"alert alert-"+t+" alert-dismissible fade"+(this.visible?" in":"")}"
role="alert">
<button type="button" class="close" @click="${async t=>this.close()}">
<span aria-hidden="true"><i class="fa fa-times-circle"></i></span>
<span class="sr-only">Close</span>
</button>
<div class="media">
<div class="media-left">
<span class="fa-stack fa-lg">
<i class="fa fa-circle fa-stack-2x"></i>
<i class="${"fa fa-"+e+" fa-stack-1x"}"></i>
</span>
</div>
<div class="media-body">
<h4 class="alert-title">${this.title}</h4>
<p class="alert-message text-pre-wrap">${this.message?this.message:""}</p>
</div>
</div>
${0===this.actions.length?"":i.html`
<div class="alert-actions">
${this.actions.map((t,e)=>i.html`
<a href="#"
title="${t.label}"
@click="${async i=>{i.preventDefault(),this.executingAction=e,await this.updateComplete,await t.action.execute(i.currentTarget),this.close()}}"
class="${s.classMap({executing:this.executingAction===e,disabled:this.executingAction>=0&&this.executingAction!==e})}"
>${t.label}</a>
`)}
</div>
`}
</div>
`}};__decorate([i.property()],l.prototype,"notificationId",void 0),__decorate([i.property()],l.prototype,"title",void 0),__decorate([i.property()],l.prototype,"message",void 0),__decorate([i.property({type:Number})],l.prototype,"severity",void 0),__decorate([i.property()],l.prototype,"duration",void 0),__decorate([i.property({type:Array,attribute:!1})],l.prototype,"actions",void 0),__decorate([i.internalProperty()],l.prototype,"visible",void 0),__decorate([i.internalProperty()],l.prototype,"executingAction",void 0),l=__decorate([i.customElement("typo3-notification-message")],l);try{parent&&parent.window.TYPO3&&parent.window.TYPO3.Notification&&(c=parent.window.TYPO3.Notification),top&&top.TYPO3.Notification&&(c=top.TYPO3.Notification)}catch(t){}return c||(c=r,"undefined"!=typeof TYPO3&&(TYPO3.Notification=c)),c}));
\ No newline at end of file
......@@ -10,4 +10,4 @@
*
* The TYPO3 project - inspiring people to share!
*/
var __importDefault=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};define(["require","exports","jquery","TYPO3/CMS/Backend/ActionButton/DeferredAction","TYPO3/CMS/Backend/ActionButton/ImmediateAction","TYPO3/CMS/Backend/Notification"],(function(e,t,a,o,i,c){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),a=__importDefault(a),describe("TYPO3/CMS/Backend/Notification:",()=>{beforeEach(()=>{a.default.fx.off=!0,jasmine.clock().install();const e=document.getElementById("alert-container");for(;null!==e&&e.firstChild;)e.removeChild(e.firstChild)}),afterEach(()=>{jasmine.clock().uninstall()}),describe("can render notifications with dismiss after 1000ms",()=>{for(let e of[{method:c.notice,title:"Notice message",message:"This notification describes a notice",class:"alert-notice"},{method:c.info,title:"Info message",message:"This notification describes an informative action",class:"alert-info"},{method:c.success,title:"Success message",message:"This notification describes a successful action",class:"alert-success"},{method:c.warning,title:"Warning message",message:"This notification describes a harmful action",class:"alert-warning"},{method:c.error,title:"Error message",message:"This notification describes an erroneous action",class:"alert-danger"}])it("can render a notification of type "+e.class,()=>{e.method(e.title,e.message,1);const t="div.alert."+e.class,a=document.querySelector(t);expect(a).not.toBe(null),expect(a.querySelector(".alert-title").textContent).toEqual(e.title),expect(a.querySelector(".alert-message").textContent).toEqual(e.message),jasmine.clock().tick(1200),expect(document.querySelector(t)).toBe(null)})}),it("can render action buttons",()=>{c.info("Info message","Some text",1,[{label:"My action",action:new i(e=>e)},{label:"My other action",action:new o(e=>e)}]);const e=document.querySelector("div.alert");expect(e.querySelector(".alert-actions")).not.toBe(null),expect(e.querySelectorAll(".alert-actions a").length).toEqual(2),expect(e.querySelectorAll(".alert-actions a")[0].textContent).toEqual("My action"),expect(e.querySelectorAll(".alert-actions a")[1].textContent).toEqual("My other action")}),it("immediate action is called",()=>{const e={callback:()=>{}};spyOn(e,"callback").and.callThrough(),c.info("Info message","Some text",1,[{label:"My immediate action",action:new i(e.callback)}]);document.querySelector("div.alert").querySelector(".alert-actions a").click(),expect(e.callback).toHaveBeenCalled()})})}));
\ No newline at end of file
define(["require","exports","TYPO3/CMS/Backend/ActionButton/DeferredAction","TYPO3/CMS/Backend/ActionButton/ImmediateAction","TYPO3/CMS/Backend/Notification"],(function(e,t,a,o,i){"use strict";Object.defineProperty(t,"__esModule",{value:!0}),describe("TYPO3/CMS/Backend/Notification:",()=>{beforeEach(()=>{const e=document.getElementById("alert-container");for(;null!==e&&e.firstChild;)e.removeChild(e.firstChild)}),describe("can render notifications with dismiss after 1000ms",()=>{for(let e of[{method:i.notice,title:"Notice message",message:"This notification describes a notice",class:"alert-notice"},{method:i.info,title:"Info message",message:"This notification describes an informative action",class:"alert-info"},{method:i.success,title:"Success message",message:"This notification describes a successful action",class:"alert-success"},{method:i.warning,title:"Warning message",message:"This notification describes a harmful action",class:"alert-warning"},{method:i.error,title:"Error message",message:"This notification describes an erroneous action",class:"alert-danger"}])it("can render a notification of type "+e.class,async()=>{e.method(e.title,e.message,1),await document.querySelector("#alert-container typo3-notification-message:last-child").updateComplete;const t="div.alert."+e.class,a=document.querySelector(t);expect(a).not.toBe(null),expect(a.querySelector(".alert-title").textContent).toEqual(e.title),expect(a.querySelector(".alert-message").textContent).toEqual(e.message),await new Promise(e=>window.setTimeout(e,2e3)),expect(document.querySelector(t)).toBe(null)})}),it("can render action buttons",async()=>{i.info("Info message","Some text",1,[{label:"My action",action:new o(e=>e)},{label:"My other action",action:new a(e=>e)}]),await document.querySelector("#alert-container typo3-notification-message:last-child").updateComplete;const e=document.querySelector("div.alert");expect(e.querySelector(".alert-actions")).not.toBe(null),expect(e.querySelectorAll(".alert-actions a").length).toEqual(2),expect(e.querySelectorAll(".alert-actions a")[0].textContent).toEqual("My action"),expect(e.querySelectorAll(".alert-actions a")[1].textContent).toEqual("My other action")}),it("immediate action is called",async()=>{const e={callback:()=>{}};spyOn(e,"callback").and.callThrough(),i.info("Info message","Some text",1,[{label:"My immediate action",action:new o(e.callback)}]),await document.querySelector("#alert-container typo3-notification-message:last-child").updateComplete;document.querySelector("div.alert").querySelector(".alert-actions a").click(),await document.querySelector("#alert-container typo3-notification-message:last-child").updateComplete,expect(e.callback).toHaveBeenCalled()})})}));
\ No newline at end of file
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment