Skip to content

Commit f4a55ad

Browse files
committed
internal: add shared post action control
1 parent b54437f commit f4a55ad

File tree

4 files changed

+320
-0
lines changed

4 files changed

+320
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import type { InternalConfirmDialog } from '../InternalConfirmDialog/InternalConfirmDialog';
2+
import type { NotificationElement } from '@vaadin/vaadin-notification';
3+
import type { ButtonElement } from '@vaadin/vaadin-button';
4+
import type { FetchEvent } from '../../public/NucleonElement/FetchEvent';
5+
6+
import './index';
7+
8+
import { InternalPostActionControl as Control } from './InternalPostActionControl';
9+
import { html, expect, fixture, waitUntil } from '@open-wc/testing';
10+
import { DialogHideEvent } from '../../private/Dialog/DialogHideEvent';
11+
import { stub } from 'sinon';
12+
13+
describe('InternalPostActionControl', () => {
14+
it('imports dependencies', () => {
15+
expect(customElements.get('vaadin-notification')).to.exist;
16+
expect(customElements.get('vaadin-button')).to.exist;
17+
expect(customElements.get('foxy-internal-confirm-dialog')).to.exist;
18+
expect(customElements.get('foxy-internal-control')).to.exist;
19+
expect(customElements.get('foxy-i18n')).to.exist;
20+
});
21+
22+
it('defines itself as foxy-internal-post-action-control', () => {
23+
expect(customElements.get('foxy-internal-post-action-control')).to.equal(Control);
24+
});
25+
26+
it('defines reactive properties', () => {
27+
expect(new Control()).to.have.property('theme', null);
28+
expect(Control.properties).to.have.property('theme');
29+
30+
expect(new Control()).to.have.property('href', null);
31+
expect(Control.properties).to.have.property('href');
32+
33+
expect(new Control()).to.have.deep.property('messageOptions', {});
34+
expect(Control.properties).to.have.deep.property('messageOptions', {
35+
attribute: 'message-options',
36+
type: Object,
37+
});
38+
});
39+
40+
it('extends foxy-internal-control', () => {
41+
expect(new Control()).to.be.instanceOf(customElements.get('foxy-internal-control'));
42+
});
43+
44+
it('renders internal confirm dialog', async () => {
45+
const control = await fixture<Control>(
46+
html`
47+
<foxy-internal-post-action-control .messageOptions=${{ foo: 'bar' }}>
48+
</foxy-internal-post-action-control>
49+
`
50+
);
51+
52+
const dialog = control.renderRoot.querySelector('foxy-internal-confirm-dialog');
53+
54+
expect(dialog).to.exist;
55+
expect(dialog).to.have.attribute('header', 'header');
56+
expect(dialog).to.have.attribute('infer', 'confirm-dialog');
57+
expect(dialog).to.have.deep.property('messageOptions', { foo: 'bar' });
58+
});
59+
60+
['success', 'error'].forEach(theme => {
61+
it(`renders ${theme} notification`, async () => {
62+
const control = await fixture<Control>(
63+
html`
64+
<foxy-internal-post-action-control lang="es" ns="post-action">
65+
</foxy-internal-post-action-control>
66+
`
67+
);
68+
69+
const notification = control.renderRoot.querySelector<NotificationElement>(
70+
`#${theme}-notification`
71+
);
72+
73+
expect(notification).to.exist;
74+
expect(notification).to.have.attribute('position', 'bottom-end');
75+
expect(notification).to.have.attribute('duration', '3000');
76+
expect(notification).to.have.attribute('theme', theme);
77+
78+
const root = document.createElement('div');
79+
notification?.renderer?.(root);
80+
81+
const message = root.querySelector('foxy-i18n');
82+
expect(message).to.exist;
83+
expect(message).to.have.attribute('key', theme);
84+
expect(message).to.have.attribute('lang', 'es');
85+
expect(message).to.have.attribute('ns', 'post-action notification');
86+
});
87+
});
88+
89+
it('renders button', async () => {
90+
const control = await fixture<Control>(html`
91+
<foxy-internal-post-action-control theme="primary"></foxy-internal-post-action-control>
92+
`);
93+
94+
const button = control.renderRoot.querySelector('vaadin-button');
95+
expect(button).to.exist;
96+
expect(button).to.have.attribute('theme', 'primary');
97+
98+
const caption = button?.querySelector('foxy-i18n');
99+
expect(caption).to.exist;
100+
expect(caption).to.have.attribute('key', 'idle');
101+
});
102+
103+
it('opens confirm dialog on button click', async () => {
104+
const control = await fixture<Control>(html`
105+
<foxy-internal-post-action-control></foxy-internal-post-action-control>
106+
`);
107+
108+
const button = control.renderRoot.querySelector('vaadin-button') as ButtonElement;
109+
const dialog = control.renderRoot.querySelector(
110+
'foxy-internal-confirm-dialog'
111+
) as InternalConfirmDialog;
112+
113+
const showMethod = stub(dialog, 'show');
114+
button.click();
115+
116+
expect(showMethod).to.have.been.calledOnce;
117+
expect(showMethod).to.have.been.calledWith(button);
118+
});
119+
120+
it('sends POST request on confirm dialog submit (successful response)', async () => {
121+
let lastSuccessEvent = null as CustomEvent | null;
122+
let lastFetchEvent = null as FetchEvent | null;
123+
124+
const control = await fixture<Control>(html`
125+
<foxy-internal-post-action-control
126+
href="https://demo.api/virtual/empty"
127+
@success=${(evt: CustomEvent) => (lastSuccessEvent = evt)}
128+
@fetch=${(evt: FetchEvent) => {
129+
lastFetchEvent = evt;
130+
evt.respondWith(Promise.resolve(new Response()));
131+
}}
132+
>
133+
</foxy-internal-post-action-control>
134+
`);
135+
136+
const dialog = control.renderRoot.querySelector(
137+
'foxy-internal-confirm-dialog'
138+
) as InternalConfirmDialog;
139+
140+
dialog.dispatchEvent(new DialogHideEvent(false));
141+
await control.requestUpdate();
142+
const button = control.renderRoot.querySelector('vaadin-button') as ButtonElement;
143+
const caption = button.querySelector('foxy-i18n');
144+
expect(button).to.have.attribute('disabled');
145+
expect(caption).to.have.attribute('key', 'busy');
146+
147+
await waitUntil(() => lastFetchEvent !== null);
148+
expect(lastFetchEvent?.request.url).to.equal('https://demo.api/virtual/empty');
149+
expect(lastFetchEvent?.request.method).to.equal('POST');
150+
151+
await waitUntil(() => lastSuccessEvent !== null);
152+
await control.requestUpdate();
153+
expect(button).to.not.have.attribute('disabled');
154+
expect(caption).to.have.attribute('key', 'idle');
155+
});
156+
157+
it('sends POST request on confirm dialog submit (failed response)', async () => {
158+
let lastErrorEvent = null as CustomEvent | null;
159+
let lastFetchEvent = null as FetchEvent | null;
160+
161+
const control = await fixture<Control>(html`
162+
<foxy-internal-post-action-control
163+
href="https://demo.api/virtual/empty"
164+
@error=${(evt: CustomEvent) => (lastErrorEvent = evt)}
165+
@fetch=${(evt: FetchEvent) => {
166+
lastFetchEvent = evt;
167+
evt.respondWith(Promise.resolve(new Response(null, { status: 400 })));
168+
}}
169+
>
170+
</foxy-internal-post-action-control>
171+
`);
172+
173+
const dialog = control.renderRoot.querySelector(
174+
'foxy-internal-confirm-dialog'
175+
) as InternalConfirmDialog;
176+
177+
dialog.dispatchEvent(new DialogHideEvent(false));
178+
await control.requestUpdate();
179+
const button = control.renderRoot.querySelector('vaadin-button') as ButtonElement;
180+
const caption = button.querySelector('foxy-i18n');
181+
expect(button).to.have.attribute('disabled');
182+
expect(caption).to.have.attribute('key', 'busy');
183+
184+
await waitUntil(() => lastFetchEvent !== null);
185+
expect(lastFetchEvent?.request.url).to.equal('https://demo.api/virtual/empty');
186+
expect(lastFetchEvent?.request.method).to.equal('POST');
187+
188+
await waitUntil(() => lastErrorEvent !== null);
189+
await control.requestUpdate();
190+
expect(button).to.not.have.attribute('disabled');
191+
expect(caption).to.have.attribute('key', 'idle');
192+
});
193+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { PropertyDeclarations, TemplateResult } from 'lit-element';
2+
import type { InternalConfirmDialog } from '../InternalConfirmDialog/InternalConfirmDialog';
3+
import type { NotificationElement } from '@vaadin/vaadin-notification';
4+
import type { DialogHideEvent } from '../../private/Dialog/DialogHideEvent';
5+
import type { ButtonElement } from '@vaadin/vaadin-button';
6+
7+
import { InternalControl } from '../InternalControl/InternalControl';
8+
import { html, render } from 'lit-html';
9+
import { ifDefined } from 'lit-html/directives/if-defined';
10+
import { API } from '../../public/NucleonElement/API';
11+
12+
export class InternalPostActionControl extends InternalControl {
13+
static get properties(): PropertyDeclarations {
14+
return {
15+
...super.properties,
16+
messageOptions: { type: Object, attribute: 'message-options' },
17+
theme: {},
18+
href: {},
19+
__buttonState: {},
20+
};
21+
}
22+
23+
messageOptions: Record<string, string> = {};
24+
25+
theme: string | null = null;
26+
27+
href: string | null = null;
28+
29+
private __buttonState: 'idle' | 'busy' = 'idle';
30+
31+
private readonly __api = new API(this);
32+
33+
renderControl(): TemplateResult {
34+
return html`
35+
<foxy-internal-confirm-dialog
36+
header="header"
37+
infer="confirm-dialog"
38+
id="confirm-dialog"
39+
.messageOptions=${this.messageOptions}
40+
@hide=${(evt: DialogHideEvent) => {
41+
if (!evt.detail.cancelled) this.__sendPost();
42+
}}
43+
>
44+
</foxy-internal-confirm-dialog>
45+
46+
<vaadin-notification
47+
position="bottom-end"
48+
duration="3000"
49+
theme="success"
50+
id="success-notification"
51+
.renderer=${this.__getNotificationRenderer('success')}
52+
>
53+
</vaadin-notification>
54+
55+
<vaadin-notification
56+
position="bottom-end"
57+
duration="3000"
58+
theme="error"
59+
id="error-notification"
60+
.renderer=${this.__getNotificationRenderer('error')}
61+
>
62+
</vaadin-notification>
63+
64+
<vaadin-button
65+
theme=${ifDefined(this.theme ?? void 0)}
66+
?disabled=${this.disabled || this.readonly || this.__buttonState !== 'idle'}
67+
@click=${(evt: CustomEvent) => {
68+
const button = evt.currentTarget as ButtonElement;
69+
const dialog = this.renderRoot.querySelector<InternalConfirmDialog>('#confirm-dialog');
70+
dialog?.show(button);
71+
}}
72+
>
73+
<foxy-i18n infer="button" key=${this.__buttonState}></foxy-i18n>
74+
</vaadin-button>
75+
`;
76+
}
77+
78+
private async __sendPost() {
79+
if (this.href && this.__buttonState === 'idle') {
80+
this.__buttonState = 'busy';
81+
82+
const response = await this.__api.fetch(this.href, { method: 'POST' });
83+
const result = response.ok ? 'success' : 'error';
84+
const selector = `#${result}-notification`;
85+
const notification = this.renderRoot.querySelector<NotificationElement>(selector);
86+
87+
notification?.open();
88+
this.__buttonState = 'idle';
89+
this.dispatchEvent(new CustomEvent(result));
90+
}
91+
}
92+
93+
private __getNotificationRenderer(state: 'success' | 'error') {
94+
return (root: HTMLElement) => {
95+
if (!root.firstElementChild) root.innerHTML = '<span></span>';
96+
97+
const layout = html`
98+
<foxy-i18n
99+
style="color: var(--lumo-${state}-contrast-color)"
100+
lang=${this.lang}
101+
key=${state}
102+
ns="${this.ns} notification"
103+
>
104+
</foxy-i18n>
105+
`;
106+
107+
render(layout, root.firstElementChild!);
108+
};
109+
}
110+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import '@vaadin/vaadin-notification';
2+
import '@vaadin/vaadin-button';
3+
4+
import '../InternalConfirmDialog/index';
5+
import '../InternalControl/index';
6+
7+
import '../../public/I18n/index';
8+
9+
import { InternalPostActionControl } from './InternalPostActionControl';
10+
11+
customElements.define('foxy-internal-post-action-control', InternalPostActionControl);
12+
13+
export { InternalPostActionControl };

web-test-runner.groups.js

+4
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ export const groups = [
7979
name: 'foxy-internal-password-control',
8080
files: './src/elements/internal/InternalPasswordControl/**/*.test.ts',
8181
},
82+
{
83+
name: 'foxy-internal-post-action-control',
84+
files: './src/elements/internal/InternalPostActionControl/**/*.test.ts',
85+
},
8286
{
8387
name: 'foxy-internal-query-builder-control',
8488
files: './src/elements/internal/InternalQueryBuilderControl/**/*.test.ts',

0 commit comments

Comments
 (0)