summaryrefslogtreecommitdiff
path: root/addons/web/static/tests/core/owl_dialog_tests.js
blob: cddb7ac7244e79c0fb83efdb8277fde479434aad (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
odoo.define('web.owl_dialog_tests', function (require) {
    "use strict";

    const LegacyDialog = require('web.Dialog');
    const makeTestEnvironment = require('web.test_env');
    const Dialog = require('web.OwlDialog');
    const testUtils = require('web.test_utils');

    const { Component, tags, useState } = owl;
    const EscapeKey = { key: 'Escape', keyCode: 27, which: 27 };
    const { xml } = tags;

    QUnit.module('core', {}, function () {
        QUnit.module('OwlDialog');

        QUnit.test("Rendering of all props", async function (assert) {
            assert.expect(35);

            class SubComponent extends Component {
                // Handlers
                _onClick() {
                    assert.step('subcomponent_clicked');
                }
            }
            SubComponent.template = xml`<div class="o_subcomponent" t-esc="props.text" t-on-click="_onClick"/>`;

            class Parent extends Component {
                constructor() {
                    super(...arguments);
                    this.state = useState({ textContent: "sup" });
                }
                // Handlers
                _onButtonClicked(ev) {
                    assert.step('button_clicked');
                }
                _onDialogClosed() {
                    assert.step('dialog_closed');
                }
            }
            Parent.components = { Dialog, SubComponent };
            Parent.env = makeTestEnvironment();
            Parent.template = xml`
                <Dialog
                    backdrop="state.backdrop"
                    contentClass="state.contentClass"
                    fullscreen="state.fullscreen"
                    renderFooter="state.renderFooter"
                    renderHeader="state.renderHeader"
                    size="state.size"
                    subtitle="state.subtitle"
                    technical="state.technical"
                    title="state.title"
                    t-on-dialog-closed="_onDialogClosed"
                    >
                    <SubComponent text="state.textContent"/>
                    <t t-set="buttons">
                        <button class="btn btn-primary" t-on-click="_onButtonClicked">The Button</button>
                    </t>
                </Dialog>`;

            const parent = new Parent();
            await parent.mount(testUtils.prepareTarget());
            const dialog = document.querySelector('.o_dialog');

            // Helper function
            async function changeProps(key, value) {
                parent.state[key] = value;
                await testUtils.nextTick();
            }

            // Basic layout with default properties
            assert.containsOnce(dialog, '.modal.o_technical_modal');
            assert.hasClass(dialog.querySelector('.modal .modal-dialog'), 'modal-lg');
            assert.containsOnce(dialog, '.modal-header > button.close');
            assert.containsOnce(dialog, '.modal-footer > button.btn.btn-primary');
            assert.strictEqual(dialog.querySelector('.modal-body').innerText.trim(), "sup",
                "Subcomponent should match with its given text");

            // Backdrop (default: 'static')
            // Static backdrop click should focus first button
            // => we need to reset that property
            dialog.querySelector('.btn-primary').blur(); // Remove the focus explicitely
            assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
            assert.notEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)'); // ... but a non transparent modal
            await testUtils.dom.click(dialog.querySelector('.modal'));
            assert.strictEqual(document.activeElement, dialog.querySelector('.btn-primary'),
                "Button should be focused when clicking on backdrop");
            assert.verifySteps([]); // Ensure not closed
            dialog.querySelector('.btn-primary').blur(); // Remove the focus explicitely

            await changeProps('backdrop', false);
            assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
            assert.strictEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)');
            await testUtils.dom.click(dialog.querySelector('.modal'));
            assert.notEqual(document.activeElement, dialog.querySelector('.btn-primary'),
                "Button should not be focused when clicking on backdrop 'false'");
            assert.verifySteps([]); // Ensure not closed

            await changeProps('backdrop', true);
            assert.containsNone(document.body, '.modal-backdrop'); // No backdrop *element* for Odoo modal...
            assert.notEqual(window.getComputedStyle(dialog.querySelector('.modal')).backgroundColor, 'rgba(0, 0, 0, 0)'); // ... but a non transparent modal
            await testUtils.dom.click(dialog.querySelector('.modal'));
            assert.notEqual(document.activeElement, dialog.querySelector('.btn-primary'),
                "Button should not be focused when clicking on backdrop 'true'");
            assert.verifySteps(['dialog_closed']);

            // Dialog class (default: '')
            await changeProps('contentClass', 'my_dialog_class');
            assert.hasClass(dialog.querySelector('.modal-content'), 'my_dialog_class');

            // Full screen (default: false)
            assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_modal_full');
            await changeProps('fullscreen', true);
            assert.hasClass(dialog.querySelector('.modal'), 'o_modal_full');

            // Size class (default: 'large')
            await changeProps('size', 'extra-large');
            assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-xl',
                "Modal should have taken the class modal-xl");
            await changeProps('size', 'medium');
            assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog',
                "Modal should not have any additionnal class with 'medium'");
            await changeProps('size', 'small');
            assert.strictEqual(dialog.querySelector('.modal-dialog').className, 'modal-dialog modal-sm',
                "Modal should have taken the class modal-sm");

            // Subtitle (default: '')
            await changeProps('subtitle', "The Subtitle");
            assert.strictEqual(dialog.querySelector('span.o_subtitle').innerText.trim(), "The Subtitle",
                "Subtitle should match with its given text");

            // Technical (default: true)
            assert.hasClass(dialog.querySelector('.modal'), 'o_technical_modal');
            await changeProps('technical', false);
            assert.doesNotHaveClass(dialog.querySelector('.modal'), 'o_technical_modal');

            // Title (default: 'Odoo')
            assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "Odoo" + "The Subtitle",
                "Title should match with its default text");
            await changeProps('title', "The Title");
            assert.strictEqual(dialog.querySelector('h4.modal-title').innerText.trim(), "The Title" + "The Subtitle",
                "Title should match with its given text");

            // Reactivity of buttons
            await testUtils.dom.click(dialog.querySelector('.modal-footer .btn-primary'));

            // Render footer (default: true)
            await changeProps('renderFooter', false);
            assert.containsNone(dialog, '.modal-footer');

            // Render header (default: true)
            await changeProps('renderHeader', false);
            assert.containsNone(dialog, '.header');

            // Reactivity of subcomponents
            await changeProps('textContent', "wassup");
            assert.strictEqual(dialog.querySelector('.o_subcomponent').innerText.trim(), "wassup",
                "Subcomponent should match with its given text");
            await testUtils.dom.click(dialog.querySelector('.o_subcomponent'));

            assert.verifySteps(['button_clicked', 'subcomponent_clicked']);

            parent.destroy();
        });

        QUnit.test("Interactions between multiple dialogs", async function (assert) {
            assert.expect(22);

            class Parent extends Component {
                constructor() {
                    super(...arguments);
                    this.dialogIds = useState([]);
                }
                // Handlers
                _onDialogClosed(id) {
                    assert.step(`dialog_${id}_closed`);
                    this.dialogIds.splice(this.dialogIds.findIndex(d => d === id), 1);
                }
            }
            Parent.components = { Dialog };
            Parent.env = makeTestEnvironment();
            Parent.template = xml`
                <div>
                    <Dialog t-foreach="dialogIds" t-as="dialogId" t-key="dialogId"
                        contentClass="'dialog_' + dialogId"
                        t-on-dialog-closed="_onDialogClosed(dialogId)"
                    />
                </div>`;

            const parent = new Parent();
            await parent.mount(testUtils.prepareTarget());

            // Dialog 1 : Owl
            parent.dialogIds.push(1);
            await testUtils.nextTick();
            // Dialog 2 : Legacy
            new LegacyDialog(null, {}).open();
            await testUtils.nextTick();
            // Dialog 3 : Legacy
            new LegacyDialog(null, {}).open();
            await testUtils.nextTick();
            // Dialog 4 : Owl
            parent.dialogIds.push(4);
            await testUtils.nextTick();
            // Dialog 5 : Owl
            parent.dialogIds.push(5);
            await testUtils.nextTick();
            // Dialog 6 : Legacy (unopened)
            const unopenedModal = new LegacyDialog(null, {});
            await testUtils.nextTick();

            // Manually closes the last legacy dialog. Should not affect the other
            // existing dialogs (3 owl and 2 legacy).
            unopenedModal.close();

            let modals = document.querySelectorAll('.modal');
            assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
                "last dialog should have the active class");
            assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
                "active dialog should not have the legacy class");
            assert.containsN(document.body, '.o_dialog', 3);
            assert.containsN(document.body, '.o_legacy_dialog', 2);

            // Reactivity with owl dialogs
            await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey); // Press Escape

            modals = document.querySelectorAll('.modal');
            assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
                "last dialog should have the active class");
            assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
                "active dialog should not have the legacy class");
            assert.containsN(document.body, '.o_dialog', 2);
            assert.containsN(document.body, '.o_legacy_dialog', 2);

            await testUtils.dom.click(modals[modals.length - 1].querySelector('.btn.btn-primary')); // Click on 'Ok' button

            modals = document.querySelectorAll('.modal');
            assert.containsOnce(document.body, '.modal.o_legacy_dialog:not(.o_inactive_modal)',
                "active dialog should have the legacy class");
            assert.containsOnce(document.body, '.o_dialog');
            assert.containsN(document.body, '.o_legacy_dialog', 2);

            // Reactivity with legacy dialogs
            await testUtils.dom.triggerEvent(modals[modals.length - 1], 'keydown', EscapeKey);

            modals = document.querySelectorAll('.modal');
            assert.containsOnce(document.body, '.modal.o_legacy_dialog:not(.o_inactive_modal)',
                "active dialog should have the legacy class");
            assert.containsOnce(document.body, '.o_dialog');
            assert.containsOnce(document.body, '.o_legacy_dialog');

            await testUtils.dom.click(modals[modals.length - 1].querySelector('.close'));

            modals = document.querySelectorAll('.modal');
            assert.notOk(modals[modals.length - 1].classList.contains('o_inactive_modal'),
                "last dialog should have the active class");
            assert.notOk(modals[modals.length - 1].classList.contains('o_legacy_dialog'),
                "active dialog should not have the legacy class");
            assert.containsOnce(document.body, '.o_dialog');
            assert.containsNone(document.body, '.o_legacy_dialog');

            parent.unmount();

            assert.containsNone(document.body, '.modal');
            // dialog 1 is closed through the removal of its parent => no callback
            assert.verifySteps(['dialog_5_closed', 'dialog_4_closed']);

            parent.destroy();
        });
    });

    QUnit.test("Z-index toggling and interactions", async function (assert) {
        assert.expect(3);

        function createCustomModal(className) {
            const $modal = $(
                `<div role="dialog" class="${className}" tabindex="-1">
                    <div class="modal-dialog medium">
                        <div class="modal-content">
                            <main class="modal-body">The modal body</main>
                        </div>
                    </div>
                </div>`
            ).appendTo('body').modal();
            const modal = $modal[0];
            modal.destroy = function () {
                $modal.modal('hide');
                this.remove();
            };
            return modal;
        }

        class Parent extends Component {
            constructor() {
                super(...arguments);
                this.state = useState({ showSecondDialog: true });
            }
        }
        Parent.components = { Dialog };
        Parent.env = makeTestEnvironment();
        Parent.template = xml`
            <div>
                <Dialog/>
                <Dialog t-if="state.showSecondDialog"/>
            </div>`;

        const parent = new Parent();
        await parent.mount(testUtils.prepareTarget());

        const frontEndModal = createCustomModal('modal');
        const backEndModal = createCustomModal('modal o_technical_modal');

        // querySelector will target the first modal (the static one).
        const owlIndexBefore = getComputedStyle(document.querySelector('.o_dialog .modal')).zIndex;
        const feZIndexBefore = getComputedStyle(frontEndModal).zIndex;
        const beZIndexBefore = getComputedStyle(backEndModal).zIndex;

        parent.state.showSecondDialog = false;
        await testUtils.nextTick();

        assert.ok(owlIndexBefore < getComputedStyle(document.querySelector('.o_dialog .modal')).zIndex,
            "z-index of the owl dialog should be incremented since the active modal was destroyed");
        assert.strictEqual(feZIndexBefore, getComputedStyle(frontEndModal).zIndex,
            "z-index of front-end modals should not be impacted by Owl Dialog activity system");
        assert.strictEqual(beZIndexBefore, getComputedStyle(backEndModal).zIndex,
            "z-index of custom back-end modals should not be impacted by Owl Dialog activity system");

        parent.destroy();
        frontEndModal.destroy();
        backEndModal.destroy();
    });
});