Api: Et2Dialog automatic tests & some documentation

This commit is contained in:
nathan 2024-12-04 13:53:19 -07:00
parent 1980251ac9
commit b7a12136ec
6 changed files with 513 additions and 185 deletions

View File

@ -0,0 +1,258 @@
```html:preview
<et2-dialog title="Dialog" class="dialog-overview" buttons="0">
This is the dialog
</et2-dialog>
<sl-button>Open Dialog</sl-button>
<script>
const dialog = document.querySelector(".dialog-overview");
const button = dialog.nextElementSibling;
// Our dialogs always open on their own, not so good for docs
dialog.open=false;
button.addEventListener('click', () => dialog.show());
</script>
```
While most widgets are expected to be used via .XET files, Et2Dialog is primarily used via javascript, and usually with
`Et2Dialog.show_dialog()`.
Et2Dialog extends [SlDialog](https://shoelace.style/components/dialog).
```js
// All parameters are optional
const dialog = Et2Dialog.show_dialog(
/* callback (button, value) => {} or null if you're using the promise*/ null,
/* Message */ "Would you like to do the thing?",
/* Title */ "Dialog title",
/* Value */ {/* Passed to callback */},
/* Buttons */ Et2Dialog.BUTTONS_OK_CANCEL
);
// Wait for user
let [button, value] = await dialog.getComplete();
// Do stuff
// or
dialog.getComplete().then(([button, value]) =>
{
// Do stuff
});
```
In your callback or after the `getComplete()` Promise resolves, you should check which button was pressed.
```js
let callback = function (button_id)
{
if (button_id == Et2Dialog.YES_BUTTON)
{
// Do stuff
}
else if (button_id == Et2Dialog.NO_BUTTON)
{
// Other stuff
}
else if (button_id == Et2Dialog.CANCEL_BUTTON)
{
// Abort
}
};
dialog = Et2Dialog.show_dialog(
callback, "Erase the entire database?", "Break things", {}, // value
Et2Dialog.BUTTONS_YES_NO_CANCEL, Et2Dialog.WARNING_MESSAGE
);
```
The parameters for the Et2Dialog.show_dialog() are all optional.
- callback - function called when the dialog closes, or false/null.
The ID of the button will be passed. Button ID will be one of the Et2Dialog.*_BUTTON constants.
The callback is _not_ called if the user closes the dialog with the X in the corner, or presses ESC.
- message - (plain) text to display
- title - Dialog title
- value (for prompt)
- buttons - Et2Dialog BUTTONS_* constant, or an array of button settings. Use DialogButton interface.
- dialog_type - Et2Dialog *_MESSAGE constant
- icon - name of icon
Note that these methods will _not_ block program flow while waiting for user input.
## Examples
### Pre-defined dialogs
We have several pre-defined dialogs that can be easily used from javascript for specific purposes.
`Et2Dialog.alert(message, title)`, `Et2Dialog.prompt(message, title)` and `Et2Dialog.confirm(message, title)`
```html:preview
<et2-hbox>
<et2-button class="alert">Alert</et2-button>
<et2-button class="prompt">Prompt</et2-button>
<et2-button class="confirm">Confirm</et2-button>
</et2-hbox>
<script>
const alertButton = document.querySelector(".alert");
alertButton.addEventListener("click", () => {
Et2Dialog.alert("Alert dialog message", "Alert title");
});
const promptButton = document.querySelector(".prompt");
promptButton.addEventListener("click", () => {
Et2Dialog.show_prompt((button, value) => {
Et2Dialog.alert("Button: " + button+ " You entered " + value, "Prompt value");
},
"Please enter your name", "Prompt dialog"
);});
const confirmButton = document.querySelector(".confirm");
confirmButton.addEventListener("click", () => {
Et2Dialog.confirm(/* senders? */null, "Are you sure you want to delete this?", "Confirm title");
});
</script>
```
### Template
You can define a dialog inside your template, and use it as needed in your app:
```xml
<template id="dialog_example">
<!-- The rest of the application template goes here -->
<!-- destroyOnClose="false" because we intend to re-use the dialog -->
<et2-dialog id="change_owner" destroyOnClose="false" buttons="1">
<et2-select-account id="new_owner" label="New owner"></et2-select-account>
<!-- Anything can go here -->
</et2-dialog>
</template>
```
```ts
async function changeOwner(entry : { owner : number })
{
const dialog = document.querySelector("#change_owner");
dialog.show();
// Wait for answer
let [button, value] = await dialog.getComplete();
if(button)
{
entry.owner = value.new_owner;
}
}
```
Or more commonly, load a template inside the dialog with the `template` attribute:
```xml
<template id="dialog_contents">
<et2-select-account id="owner" label="Set owner"></et2-select-account>
</template>
```
```ts
async function changeOwner(entry : { owner : number })
{
// Pass egw in the constructor
let dialog = new Et2Dialog(this.egw);
dialog.transformAttributes({
template: "my_app/templates/default/dialog_contents.xet",
value: {owner: entry.owner}
});
// Add to DOM, dialog will auto-open
document.body.appendChild(dialog);
// Wait for answer
let [button, value] = await dialog.getComplete();
if(button)
{
entry.owner = value.new_owner;
}
}
```
### Buttons
The easiest way to put buttons on the dialog is to use one of the button constants: `Et2Dialog.BUTTONS_OK`,
`Et2Dialog.BUTTONS_OK_CANCEL`, `Et2Dialog.BUTTONS_YES_NO`, `Et2Dialog.BUTTONS_YES_NO_CANCEL`. This also ensures
consistancy across all dialogs.
```html:preview
<et2-hbox class="button-constants">
<et2-button class="OK">BUTTONS_OK</et2-button>
<et2-button class="OK_CANCEL">BUTTONS_OK_CANCEL</et2-button>
<et2-button class="YES_NO">BUTTONS_YES_NO</et2-button>
<et2-button class="YES_NO_CANCEL">BUTTONS_YES_NO_CANCEL</et2-button>
</et2-hbox>
<script>
const buttonBox = document.querySelector(".button-constants");
Array.from(buttonBox.children).forEach(button => {
button.addEventListener("click", () => {
Et2Dialog.show_dialog(null, button.textContent.trim() + " = " + Et2Dialog[button.textContent.trim()], "Button constant", null, Et2Dialog[button.textContent.trim()]);
});
});
</script>
```
### Custom buttons
Sometimes the pre-defined buttons are insufficient. You can provide your own list of buttons, following the
`DialogButton` interface.
```html:preview
<et2-button class="custom-buttons">Custom buttons</et2-button>
<script>
const button = document.querySelector(".custom-buttons");
const customButtons /* : DialogButton[] */ = [
// These buttons will use the callback or getComplete() Promise, just like normal.
{label: "OK", id: "OK", default: true},
{label: "Yes", id: "Yes"},
{label: "Sure", id: "Sure", disabled: true},
{label: "Maybe", click: function() {
// If you override the click handler, 'this' will be the dialog.
// Things get more complicated, so doing this is not recommended
}
},
{label: "Negative choice", id:"No", align: "right"}
];
button.addEventListener("click", () => {
let dialog = Et2Dialog.show_dialog(null, "Custom buttons", "Custom buttons", null, customButtons);
});
</script>
```
```ts
// Pass egw in the constructor
let dialog = new Et2Dialog(my_egw_reference);
// Set attributes. They can be set in any way, but this is convenient.
dialog.transformAttributes({
// If you use a template, the second parameter will be the value of the template, as if it were submitted.
callback: function(button_id, value) {...}, // return false to prevent dialog closing
buttons: [
// These ones will use the callback, just like normal. Use DialogButton interface.
{label: egw.lang("OK"), id: "OK", default: true},
{label: egw.lang("Yes"), id: "Yes"},
{label: egw.lang("Sure"), id: "Sure"},
{
label: egw.lang("Maybe"), click: function()
{
// If you override, 'this' will be the dialog DOMNode.
// Things get more complicated.
// Do what you like here
}
},
],
title: 'Why would you want to do this?',
template: "/egroupware/addressbook/templates/default/edit.xet",
value: {content: {...default values}, sel_options: {...}...}
});
// Add to DOM, dialog will auto-open
document.body.appendChild(dialog);
// If you want, wait for close
let result = await dialog.getComplete();
```

View File

@ -15,7 +15,6 @@ import {classMap} from "lit/directives/class-map.js";
import {ifDefined} from "lit/directives/if-defined.js";
import {repeat} from "lit/directives/repeat.js";
import {styleMap} from "lit/directives/style-map.js";
import {et2_template} from "../et2_widget_template";
import type {etemplate2} from "../etemplate2";
import {egw, IegwAppLocal} from "../../jsapi/egw_global";
import interact from "@interactjs/interactjs";
@ -25,6 +24,7 @@ import shoelace from "../Styles/shoelace";
import {SlDialog} from "@shoelace-style/shoelace";
import {egwIsMobile} from "../../egw_action/egw_action_common";
import {waitForEvent} from "../Et2Widget/event";
import {property} from "lit/decorators/property.js";
export interface DialogButton
{
@ -40,90 +40,14 @@ export interface DialogButton
/**
* A common dialog widget that makes it easy to inform users or prompt for information.
*
* It is possible to have a custom dialog by using a template, but you can also use
* the static method Et2Dialog.show_dialog(). At its simplest, you can just use:
* ```ts
* Et2Dialog.show_dialog(false, "Operation completed");
* ```
* @slot - The dialog's main content
* @slot label - The dialog's title. Alternatively, you can use the title attribute.
* @slot header-actions - Optional actions to add to the header. Works best with <et2-button-icon>
* @slot footer - The dialog's footer, where we put the buttons.
*
* Or a more complete example:
* ```js
* let callback = function (button_id)
* {
* if(button_id == Et2Dialog.YES_BUTTON)
* {
* // Do stuff
* }
* else if (button_id == Et2Dialog.NO_BUTTON)
* {
* // Other stuff
* }
* else if (button_id == Et2Dialog.CANCEL_BUTTON)
* {
* // Abort
* }
* }.
* let dialog = Et2Dialog.show_dialog(
* callback, "Erase the entire database?","Break things", {} // value
* et2_dialog.BUTTONS_YES_NO_CANCEL, et2_dialog.WARNING_MESSAGE
* );
* ```
*
* Or, using Promises instead of a callback:
* ```ts
* let result = await Et2Dialog.show_prompt(null, "Name").getComplete();
* if(result.button_id == Et2Dialog.OK_BUTTON)
* {
* // Do stuff with result.value
* }
* ```
*
* The parameters for the above are all optional, except callback (which can be null) and message:
* - callback - function called when the dialog closes, or false/null.
* The ID of the button will be passed. Button ID will be one of the Et2Dialog.*_BUTTON constants.
* The callback is _not_ called if the user closes the dialog with the X in the corner, or presses ESC.
* - message - (plain) text to display
* - title - Dialog title
* - value (for prompt)
* - buttons - Et2Dialog BUTTONS_* constant, or an array of button settings. Use DialogButton interface.
* - dialog_type - Et2Dialog *_MESSAGE constant
* - icon - URL of icon
*
* Note that these methods will _not_ block program flow while waiting for user input unless you use "await" on getComplete().
* The user's input will be provided to the callback.
*
* You can also create a custom dialog using an etemplate, even setting all the buttons yourself.
* ```ts
* // Pass egw in the constructor
* let dialog = new Et2Dialog(my_egw_reference);
*
* // Set attributes. They can be set in any way, but this is convenient.
* dialog.transformAttributes({
* // If you use a template, the second parameter will be the value of the template, as if it were submitted.
* callback: function(button_id, value) {...}, // return false to prevent dialog closing
* buttons: [
* // These ones will use the callback, just like normal. Use DialogButton interface.
* {label: egw.lang("OK"),id:"OK", default: true},
* {label: egw.lang("Yes"),id:"Yes"},
* {label: egw.lang("Sure"),id:"Sure"},
* {label: egw.lang("Maybe"),click: function() {
* // If you override, 'this' will be the dialog DOMNode.
* // Things get more complicated.
* // Do what you like here
* }},
*
* ],
* title: 'Why would you want to do this?',
* template:"/egroupware/addressbook/templates/default/edit.xet",
* value: { content: {...default values}, sel_options: {...}...}
* });
* // Add to DOM, dialog will auto-open
* document.body.appendChild(dialog);
* // If you want, wait for close
* let result = await dialog.getComplete();
*```
*
* Customize initial focus by setting the "autofocus" attribute on a control, otherwise first input will have focus
* @event open - Emitted when the dialog opens
* @event close - Emitted when the dialog closes
* @event before-load - Emitted before the dialog opens
*/
export class Et2Dialog extends Et2Widget(SlDialog)
{
@ -144,6 +68,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
* @protected
* @internal
*/
private __template : string; // Name
protected _template_widget : etemplate2 | null;
protected _template_promise : Promise<boolean>;
@ -274,64 +199,72 @@ export class Et2Dialog extends Et2Widget(SlDialog)
];
}
static get properties()
{
return {
...super.properties,
callback: Function,
/**
* Function called when the dialog is closed
*
* Wait for dialog.getComplete() instead
*/
@property({type: Function})
callback : Function;
/**
* Allow other controls to be accessed while the dialog is visible
* while not conflicting with internal attribute
*/
isModal: {type: Boolean, reflect: true},
/**
* Allow other controls to be accessed while the dialog is visible
* while not conflicting with internal attribute
*/
@property({type: Boolean, reflect: true})
isModal : boolean;
/**
* Title for the dialog, goes in the header
*/
title: String,
/**
* Pre-defined group of buttons, one of the BUTTONS_*
*/
@property({type: Number})
buttons : Number;
/**
* Pre-defined group of buttons, one of the BUTTONS_*
*/
buttons: Number,
// Force size on the dialog. Normally it sizes to content.
@property({type: Number})
width : number;
// Force size on the dialog. Normally it sizes to content.
@property({type: Number})
height : number;
/**
* Instead of a message, show this template file instead
*/
template: String,
/**
* Message to show to user
*/
@property({type: String})
message : string;
// Force size on the dialog. Normally it sizes to content.
width: Number,
height: Number,
/**
* Pre-defined dialog styles
*/
@property({type: Number})
dialog_type : number;
// We just pass these on to Et2DialogContent
message: String,
dialog_type: Number,
icon: String,
value: Object,
/**
* Include an icon on the dialog
*
* @type {string}
*/
@property({type: String})
icon : string;
/**
* Automatically destroy the dialog when it closes. Set to false to keep the dialog around.
*/
destroyOnClose: Boolean,
/**
* Automatically destroy the dialog when it closes. Set to false to keep the dialog around.
*/
@property({type: Boolean})
destroyOnClose : boolean;
/**
* Legacy-option for appending dialog into a specific dom node
*/
appendTo: String,
/**
* When it's set to false dialog won't get closed by hitting Esc
*/
@property({type: Boolean})
hideOnEscape : boolean;
/**
* When it's set to false dialog won't get closed by hitting Esc
*/
hideOnEscape: Boolean,
/**
* When set to true it removes the close button from dialog's header
*/
@property({type: Boolean, reflect: true})
noCloseButton : boolean;
/**
* When set to true it removes the close button from dialog's header
*/
noCloseButton: {type: Boolean, reflect: true}
}
}
/*
* List of properties that get translated
@ -456,9 +389,9 @@ export class Et2Dialog extends Et2Widget(SlDialog)
destroy()
{
if(this.template)
if(this._template_widget)
{
this.template.clear(true);
this._template_widget.clear(true);
}
this.remove();
}
@ -476,7 +409,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
addOpenListeners()
{
//super.addOpenListeners();
super.addOpenListeners();
// Bind on the ancestor, not the buttons, so their click handler gets a chance to run
this.addEventListener("click", this._onButtonClick);
@ -485,7 +418,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
removeOpenListeners()
{
//super.removeOpenListeners();
super.removeOpenListeners();
this.removeEventListener("click", this._onButtonClick);
this.removeEventListener("keydown", this.handleKeyDown);
}
@ -707,12 +640,17 @@ export class Et2Dialog extends Et2Widget(SlDialog)
return this.value;
}
set value(new_value)
@property({type: Object})
set value(new_value : Object)
{
this.__value = new_value;
}
set template(new_template_name)
/**
* Instead of a simple message, show this template file instead
*/
@property({type: String})
set template(new_template_name : string)
{
let old_template = this.__template;
this.__template = new_template_name;
@ -732,16 +670,20 @@ export class Et2Dialog extends Et2Widget(SlDialog)
get template()
{
// Can't return undefined or requestUpdate() will not notice a change
return this._template_widget || null;
return this.__template || null;
}
get title() : string { return this.label }
/**
* Title for the dialog, goes in the header
*/
@property()
set title(new_title : string)
{
this.label = new_title;
}
get title() : string { return this.label }
updated(changedProperties)
{
super.updated(changedProperties);
@ -1004,28 +946,35 @@ export class Et2Dialog extends Et2Widget(SlDialog)
{
return;
}
if(this.template)
if(this._template_widget && typeof this._template_widget.focusOnFirstInput == "function")
{
this.template.focusOnFirstInput();
this._template_widget.focusOnFirstInput();
}
else
{
// Not a template, but maybe something?
const $input = jQuery('input:visible,et2-textbox:visible,et2-select-email:visible', this)
// Date fields open the calendar popup on focus
.not('.et2_date')
.filter(function()
const input = Array.from(this.querySelectorAll('input,et2-textbox,et2-select-email')).filter(element =>
{
// Skip invisible
if(!element.checkVisibility())
{
// Skip inputs that are out of tab ordering
const $this = jQuery(this);
return !$this.attr('tabindex') || parseInt($this.attr('tabIndex')) >= 0;
}).first();
return false;
}
// Date fields open the calendar popup on focus
if(element.classList.contains("et2_date"))
{
return false;
}
// Skip inputs that are out of tab ordering
return !element.hasAttribute('tabindex') || parseInt(element.getAttribute('tabIndex')) >= 0
}).pop();
// mobile device, focus only if the field is empty (usually means new entry)
// should focus always for non-mobile one
if(egwIsMobile() && $input.val() == "" || !egwIsMobile())
if(input && (egwIsMobile() && typeof input.getValue == "function" && input.getValue() == "" || !egwIsMobile()))
{
$input.focus();
input.focus();
}
}
}
@ -1118,7 +1067,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
/**
* Show a confirmation dialog
*
* @param {function} _callback Function called when the user clicks a button. The context will be the et2_dialog widget, and the button constant is passed in.
* @param {function} _callback Function called when the user clicks a button. The context will be the Et2Dialog widget, and the button constant is passed in.
* @param {string} _message Message to be place in the dialog.
* @param {string} _title Text in the top bar of the dialog.
* @param _value passed unchanged to callback as 2. parameter
@ -1277,7 +1226,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
* check to avoid executing more than intended.
*
* @param {function} _callback Function called when the user clicks a button,
* or when the list is done processing. The context will be the et2_dialog
* or when the list is done processing. The context will be the Et2Dialog
* widget, and the button constant is passed in.
* @param {string} _message Message to be place in the dialog. Usually just
* text, but DOM nodes will work too.
@ -1289,7 +1238,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
* address. Multiple parameters are allowed, in an array.
* @param {string|egw} _egw_or_appname egw object with already laoded translations or application name to load translations for
*
* @return {et2_dialog}
* @return {Et2Dialog}
*/
static long_task(_callback, _message, _title, _menuaction, _list, _egw_or_appname)
{
@ -1302,7 +1251,11 @@ export class Et2Dialog extends Et2Widget(SlDialog)
{
// Cancel run
cancel = true;
jQuery("button[button_id=" + Et2Dialog.CANCEL_BUTTON + "]", dialog.div.parent()).button("disable");
let button = <Et2Button>dialog.querySelector("button[button_id=" + Et2Dialog.CANCEL_BUTTON + "]");
if(button)
{
button.disabled = true;
}
updateUi.call(_list.length, '');
}
}
@ -1535,8 +1488,7 @@ export class Et2Dialog extends Et2Widget(SlDialog)
//@ts-ignore TS doesn't recognize Et2Dialog as HTMLEntry
customElements.define("et2-dialog", Et2Dialog);
// make et2_dialog publicly available as we need to call it from templates
// make Et2Dialog publicly available as we need to call it from templates
{
window['et2_dialog'] = Et2Dialog;
window['Et2Dialog'] = Et2Dialog;
}

View File

@ -0,0 +1,140 @@
import {assert, elementUpdated, expect, fixture, html, oneEvent} from '@open-wc/testing';
import {sendKeys} from "@web/test-runner-commands";
import * as sinon from 'sinon';
import {Et2Dialog} from "../Et2Dialog";
/**
* Test file for Etemplate webComponent Et2Dialog
*
* In here we test just the simple, basic widget stuff.
*/
// Stub global egw for egw_action to find
const egw = {
ajaxUrl: () => "",
app: () => "addressbook",
app_name: () => "addressbook",
decodePath: (_path : string) => _path,
image: () => "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxNS4wLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjwhRE9DVFlQRSBzdmcgUFVCTElDICItLy9XM0MvL0RURCBTVkcgMS4xLy9FTiIgImh0dHA6Ly93d3cudzMub3JnL0dyYXBoaWNzL1NWRy8xLjEvRFREL3N2ZzExLmR0ZCI+DQo8c3ZnIHZlcnNpb249IjEuMSIgaWQ9IkViZW5lXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB3aWR0aD0iMzJweCIgaGVpZ2h0PSIzMnB4IiB2aWV3Qm94PSIwIDAgMzIgMzIiIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDMyIDMyIiB4bWw6c3BhY2U9InByZXNlcnZlIj4NCjxwYXRoIGZpbGwtcnVsZT0iZXZlbm9kZCIgY2xpcC1ydWxlPSJldmVub2RkIiBmaWxsPSIjNjk2OTY5IiBkPSJNNi45NDMsMjguNDUzDQoJYzAuOTA2LDAuNzY1LDIuMDk3LDEuMTI3LDMuMjg2LDEuMTA5YzAuNDMsMC4wMTQsMC44NTItMC4wNjgsMS4yNjUtMC4yMDdjMC42NzktMC4xOCwxLjMyOC0wLjQ1LDEuODY2LTAuOTAyTDI5LjQwMywxNC45DQoJYzEuNzcyLTEuNDk4LDEuNzcyLTMuOTI1LDAtNS40MjJjLTEuNzcyLTEuNDk3LTQuNjQ2LTEuNDk3LTYuNDE4LDBMMTAuMTE5LDIwLjM0OWwtMi4zODktMi40MjRjLTEuNDQtMS40NTctMy43NzItMS40NTctNS4yMTIsMA0KCWMtMS40MzgsMS40Ni0xLjQzOCwzLjgyNSwwLDUuMjgxQzIuNTE4LDIzLjIwNiw1LjQ3NCwyNi45NDcsNi45NDMsMjguNDUzeiIvPg0KPC9zdmc+DQo=",
jsonq: () => Promise.resolve({}),
lang: i => i + "*",
link: i => i,
preference: i => "",
tooltipUnbind: () => {},
webserverUrl: ""
}
window.egw = function() {return egw};
Object.assign(window.egw, egw);
let element : Et2Dialog;
async function before()
{
// Create an element to test with, and wait until it's ready
// @ts-ignore
element = await fixture<Et2Dialog>(html`
<et2-dialog title="I'm a dialog">
</et2-dialog>
`);
// Stub egw()
sinon.stub(element, "egw").returns(window.egw);
await elementUpdated(element);
return element;
}
describe("Dialog widget basics", () =>
{
// Setup run before each test
beforeEach(before);
// Make sure it works
it('is defined', () =>
{
assert.instanceOf(element, Et2Dialog);
});
it('has a title', async() =>
{
element.title = "Title set";
await elementUpdated(element);
assert.equal(element.shadowRoot.querySelector("#title").textContent.trim(), "Title set");
});
});
describe("Properties", async() =>
{
// Setup run before each test
beforeEach(before);
it("destroyOnClose = true", async() =>
{
element.destroyOnClose = true;
await element.show();
assert.isNotNull(document.querySelector("et2-dialog"));
await element.hide();
assert.isNull(document.querySelector("et2-dialog"));
});
it("destroyOnClose = false", async() =>
{
element.destroyOnClose = false;
await element.show();
assert.isNotNull(document.querySelector("et2-dialog"));
await element.hide();
assert.isNotNull(document.querySelector("et2-dialog"));
});
it("noCloseButton", async() =>
{
await element.show();
const closeButton = element.shadowRoot.querySelector("[part=close-button]");
assert.isNotNull(closeButton);
assert.isTrue(closeButton.checkVisibility());
element.noCloseButton = true;
await element.show();
assert.isFalse(closeButton.checkVisibility());
});
it("hideOnEscape = true", async() =>
{
element.hideOnEscape = true;
await element.show();
const listener = oneEvent(element, "close");
await sendKeys({down: "Escape"});
const event = await listener;
expect(event).to.exist;
});
it("hideOnEscape = false", (done) =>
{
element.hideOnEscape = false;
element.show().then(async() =>
{
// Listen for events
const requestCloseListener = oneEvent(element, "sl-request-close");
const closeListener = oneEvent(element, "close");
let event = null;
// Press Escape
let keysSender = await sendKeys({down: "Escape"});
// Request close gets sent, but Et2Dialog cancels it if hideOnEscape=false
await requestCloseListener;
// Can't really test that an event didn't happen
setTimeout(() =>
{
assert.isNull(event, "Close happened");
done();
}, 500)
event = await closeListener;
return requestCloseListener;
});
});
});

View File

@ -1445,6 +1445,7 @@ const Et2WidgetMixin = <T extends Constructor>(superClass : T) =>
// These methods are used inside widgets, but may not always be available depending on egw() loading (tests, docs)
const required = {
debug: () => {console.log(arguments);},
image: () => "",
lang: (l) => {return l;},
preference: () => {return false;},
};

28
package-lock.json generated
View File

@ -42,6 +42,7 @@
"@web/dev-server-esbuild": "^1.0.2",
"@web/dev-server-rollup": "^0.6.3",
"@web/test-runner": "^0.18.2",
"@web/test-runner-commands": "^0.9.0",
"@web/test-runner-playwright": "^0.11.0",
"browser-sync": "^3.0.2",
"cem": "^1.0.4",
@ -4179,13 +4180,6 @@
"integrity": "sha512-ARATsLdrGPUnaBvxLhUlnltcMgn7pQG312S8ccdYlnyijabrX9RN/KN/iGj9Am96CoW8e/K9628BA7Bv4XHdrA==",
"dev": true
},
"node_modules/@types/prop-types": {
"version": "15.7.13",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz",
"integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==",
"license": "MIT",
"peer": true
},
"node_modules/@types/qs": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
@ -4198,17 +4192,6 @@
"integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==",
"dev": true
},
"node_modules/@types/react": {
"version": "18.3.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.12.tgz",
"integrity": "sha512-D2wOSq/d6Agt28q7rSI3jhU7G6aiuzljDGZ2hTZHIkrTLUI+AF3WMeKkEZ9nN2fkBAlcktT6vcZjDFiIhMYEQw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/@types/resolve": {
"version": "1.17.1",
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
@ -6907,13 +6890,6 @@
"node": ">=14"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
},
"node_modules/custom-element-jet-brains-integration": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/custom-element-jet-brains-integration/-/custom-element-jet-brains-integration-1.2.1.tgz",
@ -13287,7 +13263,7 @@
"version": "2.79.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz",
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
"devOptional": true,
"dev": true,
"bin": {
"rollup": "dist/bin/rollup"
},

View File

@ -30,6 +30,7 @@
"@web/dev-server-esbuild": "^1.0.2",
"@web/dev-server-rollup": "^0.6.3",
"@web/test-runner": "^0.18.2",
"@web/test-runner-commands": "^0.9.0",
"@web/test-runner-playwright": "^0.11.0",
"browser-sync": "^3.0.2",
"cem": "^1.0.4",