@justeattakeaway/pie-modal 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,16 @@
1
1
  import { test, expect } from '@sand4rt/experimental-ct-web';
2
+ import { type Page } from '@playwright/test';
2
3
  import { PieIconButton } from '@justeattakeaway/pie-icon-button';
4
+ import {
5
+ WebComponentTestWrapper,
6
+ } from '@justeattakeaway/pie-webc-testing/src/helpers/components/web-component-test-wrapper/WebComponentTestWrapper.ts';
7
+ import { renderTestPieModal } from '../helpers/index.ts';
8
+
3
9
  import { PieModal } from '@/index';
4
10
  import { headingLevels } from '@/defs';
5
11
 
12
+ const closeButtonSelector = '[data-test-id="modal-close-button"]';
13
+
6
14
  // Mount any components that are used inside of pie-modal so that
7
15
  // they have been registered with the browser before the tests run.
8
16
  // There is likely a nicer way to do this but this will temporarily
@@ -21,40 +29,46 @@ test.beforeEach(async ({ page, mount }) => {
21
29
  });
22
30
 
23
31
  headingLevels.forEach((headingLevel) => test(`should render the correct heading tag based on the value of headingLevel: ${headingLevel}`, async ({ mount }) => {
32
+ // Arrange
24
33
  const props = {
25
34
  heading: 'Modal Header',
26
35
  headingLevel,
27
36
  };
28
37
 
38
+ // Act
29
39
  const component = await mount(PieModal, { props });
30
40
 
41
+ // Assert
31
42
  await expect(component.locator(`${props.headingLevel}.c-modal-heading`)).toContainText(props.heading);
32
43
  }));
33
44
 
34
45
  ['span', 'section'].forEach((headingLevel) => test(`should render the fallback heading level 'h2' if invalid headingLevel: ${headingLevel} is passed`, async ({ mount }) => {
46
+ // Arrange
35
47
  const props = {
36
48
  heading: 'Modal Header',
37
- // assert type checking as we purposely provide incorrect value
38
49
  headingLevel,
39
50
  };
40
51
 
52
+ // Act
41
53
  // eslint-disable-next-line @typescript-eslint/ban-ts-comment
42
54
  // @ts-ignore // Added this as we want to deliberately test with invalid headingLevel (which is an invalid type based on ModalProps)
43
55
  const component = await mount(PieModal, { props });
44
56
 
45
- // h2 is the default / fallback value
57
+ // Assert
46
58
  await expect(component.locator('h2.c-modal-heading')).toContainText(props.heading);
47
59
  }));
48
60
 
49
- test.describe('`Pie Modal is closed`', () => {
50
- test.describe('when via the close button click', () => {
61
+ test.describe('When modal is closed', () => {
62
+ test.describe('by clicking the close button', () => {
51
63
  test('should dispatch event `pie-modal-close`', async ({ mount, page }) => {
64
+ // Arrange
52
65
  const messages: string[] = [];
53
66
  await mount(
54
67
  PieModal,
55
68
  {
56
69
  props: {
57
70
  isOpen: true,
71
+ isDismissible: true,
58
72
  },
59
73
  on: {
60
74
  click: (event: string) => messages.push(event),
@@ -62,14 +76,16 @@ test.describe('`Pie Modal is closed`', () => {
62
76
  },
63
77
  );
64
78
 
65
- await page.locator('.c-modal-closeBtn').click();
79
+ await page.locator(closeButtonSelector).click();
66
80
 
81
+ // Assert
67
82
  expect(messages).toHaveLength(1);
68
83
  });
69
84
  });
70
85
 
71
- test.describe('when via the backdrop click', () => {
86
+ test.describe('by clicking the backdrop', () => {
72
87
  test('should dispatch event `pie-modal-close`', async ({ mount, page }) => {
88
+ // Arrange
73
89
  const messages: string[] = [];
74
90
  await mount(
75
91
  PieModal,
@@ -83,10 +99,277 @@ test.describe('`Pie Modal is closed`', () => {
83
99
  },
84
100
  );
85
101
 
102
+ // Act
86
103
  await page.locator('#dialog').click();
87
104
 
105
+ // Assert
88
106
  expect(messages).toHaveLength(1);
89
107
  });
90
108
  });
109
+
110
+ test.describe('`returnFocusAfterCloseSelector` prop', () => {
111
+ test.describe('when given', () => {
112
+ test('should return focus to specified element', async ({ mount, page }) => {
113
+ // Arrange
114
+ const component = renderTestPieModal({
115
+ returnFocusAfterCloseSelector: '#focus-me',
116
+ });
117
+
118
+ await mount(WebComponentTestWrapper, {
119
+ props: {
120
+ pageMode: true,
121
+ },
122
+ slots: {
123
+ component,
124
+ pageMarkup: `<div>
125
+ <button id="default"></button>
126
+ <button id="focus-me"></button>
127
+ <button id="not-me"></button>
128
+ </div>`,
129
+ },
130
+ });
131
+
132
+ // Act
133
+ await page.locator(closeButtonSelector).click();
134
+
135
+ const focusedElement = await page.locator(':focus');
136
+ const focusedElementId = await focusedElement.getAttribute('id');
137
+
138
+ // Assert
139
+ expect(focusedElementId).toBe('focus-me');
140
+ });
141
+
142
+ test('should return focus to first matching element', async ({ page, mount }) => {
143
+ // Arrange
144
+ const component = renderTestPieModal({
145
+ returnFocusAfterCloseSelector: '[data-test-id="focus-me"]',
146
+ });
147
+
148
+ await mount(WebComponentTestWrapper, {
149
+ props: {
150
+ pageMode: true,
151
+ },
152
+ slots: {
153
+ component,
154
+ pageMarkup: `<div>
155
+ <button id="default"></button>
156
+ <button data-test-id="focus-me" id="actual-focus"></button>
157
+ <button data-test-id="focus-me"></button>
158
+ </div>`,
159
+ },
160
+ });
161
+
162
+ // Act
163
+ await page.locator(closeButtonSelector).click();
164
+
165
+ const focusedElement = await page.locator(':focus');
166
+ const focusedElementId = await focusedElement.getAttribute('id');
167
+
168
+ // Assert
169
+ expect(focusedElementId).toBe('actual-focus');
170
+ });
171
+ });
172
+
173
+ test.describe('when not given', () => {
174
+ [{
175
+ mechanism: 'close button',
176
+ modalCloseFunction: async (page : Page) => {
177
+ await page.locator(closeButtonSelector).click();
178
+ },
179
+ }, {
180
+ mechanism: 'Esc key',
181
+ modalCloseFunction: async (page : Page) => {
182
+ await page.keyboard.press('Escape');
183
+ },
184
+ }].forEach(({ mechanism, modalCloseFunction }) => {
185
+ test.describe(`and closed by the ${mechanism}`, () => {
186
+ test('should return focus to the element that opens the modal', async ({ page, mount }) => {
187
+ // Arrange
188
+ const component = renderTestPieModal({ isOpen: false });
189
+
190
+ await mount(WebComponentTestWrapper, {
191
+ props: {
192
+ pageMode: true,
193
+ },
194
+ slots: {
195
+ component,
196
+ pageMarkup: `<div>
197
+ <button id="not-me"></button>
198
+ <button data-test-id="open-modal" id="default"></button>
199
+ </div>`,
200
+ },
201
+ });
202
+
203
+ await page.evaluate(() => {
204
+ // Set up a button which opens the modal when clicked
205
+ document.querySelector('[data-test-id="open-modal"]')?.addEventListener('click', () => {
206
+ document.querySelector('pie-modal')?.setAttribute('isOpen', 'true');
207
+ });
208
+ });
209
+
210
+ // Act
211
+ await page.locator('[data-test-id="open-modal"]').click();
212
+ await modalCloseFunction(page);
213
+
214
+ const focusedElement = await page.locator(':focus');
215
+ const focusedElementId = await focusedElement.getAttribute('id');
216
+
217
+ // Assert
218
+ expect(focusedElementId).toBe('default');
219
+ });
220
+ });
221
+ });
222
+ });
223
+ });
91
224
  });
92
225
 
226
+ test.describe('`isDismissible` prop', () => {
227
+ test.describe('when `true`', () => {
228
+ test('should make the modal contain a close button', async ({ mount }) => {
229
+ // Arrange
230
+ const component = await mount(
231
+ PieModal,
232
+ {
233
+ props: {
234
+ isOpen: true,
235
+ isDismissible: true,
236
+ },
237
+ },
238
+ );
239
+
240
+ // Act & Assert
241
+ await expect(component.locator(closeButtonSelector)).toBeVisible();
242
+ });
243
+
244
+ test('should close the modal when the close button is clicked', async ({ mount }) => {
245
+ // Arrange
246
+ const component = await mount(
247
+ PieModal,
248
+ {
249
+ props: {
250
+ isOpen: true,
251
+ isDismissible: true,
252
+ },
253
+ },
254
+ );
255
+
256
+ // Act
257
+ await component.locator('[data-test-id="modal-close-button"]').click();
258
+
259
+ // Assert
260
+ await expect(component).not.toBeVisible();
261
+ });
262
+
263
+ test('should close the modal when the backdrop is clicked', async ({ mount, page }) => {
264
+ // Arrange
265
+ await mount(
266
+ PieModal,
267
+ {
268
+ props: {
269
+ isOpen: true,
270
+ isDismissible: true,
271
+ },
272
+ },
273
+ );
274
+
275
+ // Act
276
+ await page.locator('body').click();
277
+
278
+ const element = await page.locator('#dialog');
279
+
280
+ const styles = await element.evaluate((modal) => {
281
+ const computedStyles = window.getComputedStyle(modal);
282
+ return {
283
+ display: computedStyles.getPropertyValue('display'),
284
+ };
285
+ });
286
+
287
+ // Assert
288
+ expect(styles.display).toBe('none');
289
+ });
290
+
291
+ test('should close the modal when the Escape key is pressed', async ({ mount, page }) => {
292
+ // Arrange
293
+ const component = await mount(
294
+ PieModal,
295
+ {
296
+ props: {
297
+ isOpen: true,
298
+ isDismissible: false,
299
+ },
300
+ },
301
+ );
302
+
303
+ // Act
304
+ await page.keyboard.press('Escape');
305
+
306
+ // Assert
307
+ await expect(component).not.toBeVisible();
308
+ });
309
+ });
310
+
311
+ test.describe('when `isDismissible` is `false`', () => {
312
+ test('should make the modal NOT contain a close button', async ({ mount }) => {
313
+ // Arrange
314
+ const component = await mount(
315
+ PieModal,
316
+ {
317
+ props: {
318
+ isOpen: true,
319
+ isDismissible: false,
320
+ },
321
+ },
322
+ );
323
+
324
+ // Act & Assert
325
+ await expect(component.locator(closeButtonSelector)).not.toBeVisible();
326
+ });
327
+
328
+ test('should NOT close the modal when the backdrop is clicked', async ({ mount, page }) => {
329
+ // Arrange
330
+ await mount(
331
+ PieModal,
332
+ {
333
+ props: {
334
+ isOpen: true,
335
+ isDismissible: false,
336
+ },
337
+ },
338
+ );
339
+
340
+ // Act
341
+ await page.locator('body').click();
342
+
343
+ const element = await page.locator('#dialog');
344
+
345
+ const styles = await element.evaluate((modal) => {
346
+ const computedStyles = window.getComputedStyle(modal);
347
+ return {
348
+ display: computedStyles.getPropertyValue('display'),
349
+ };
350
+ });
351
+
352
+ // Assert
353
+ expect(styles.display).toBe('block');
354
+ });
355
+
356
+ test('should NOT close the modal when the Escape key is pressed', async ({ mount, page }) => {
357
+ // Arrange
358
+ const component = await mount(
359
+ PieModal,
360
+ {
361
+ props: {
362
+ isOpen: true,
363
+ isDismissible: false,
364
+ },
365
+ },
366
+ );
367
+
368
+ // Act
369
+ await page.keyboard.press('Escape');
370
+
371
+ // Assert
372
+ await expect(component.locator('dialog')).toBeVisible();
373
+ });
374
+ });
375
+ });
@@ -0,0 +1,29 @@
1
+ import { ModalProps } from '@/defs';
2
+
3
+ // Renders a <pie-modal> HTML string with the given prop values
4
+ export const renderTestPieModal = ({
5
+ heading = 'This is a modal heading',
6
+ headingLevel = 'h2',
7
+ isDismissible = true,
8
+ isFullWidthBelowMid = false,
9
+ isOpen = true,
10
+ returnFocusAfterCloseSelector = undefined,
11
+ size = 'medium',
12
+ } : Partial<ModalProps> = {}) => `<pie-modal
13
+ heading="${heading}"
14
+ headingLevel="${headingLevel}"
15
+ ${isFullWidthBelowMid ? 'isFullWidthBelowMid' : ''}
16
+ ${isDismissible ? 'isDismissible' : ''}
17
+ ${isOpen ? 'isOpen' : ''}
18
+ ${returnFocusAfterCloseSelector ? `returnFocusAfterCloseSelector=${returnFocusAfterCloseSelector}` : ''}
19
+ size="${size}">
20
+ </pie-modal>`;
21
+
22
+ // Creates some test page markup to test scroll locking
23
+ export const createScrollablePageHTML = () => `<div>
24
+ <h1>Test Page</h1>
25
+ <p> Test copy </p>
26
+ <ol>
27
+ ${'<li>List item</li>'.repeat(200)}
28
+ </ol>
29
+ </div>`;
@@ -6,19 +6,7 @@ import {
6
6
  import { PieIconButton } from '@justeattakeaway/pie-icon-button';
7
7
  import { PieModal } from '@/index';
8
8
  import { ModalProps, sizes } from '@/defs';
9
-
10
- // Renders a <pie-modal> HTML string with the given prop values
11
- const renderTestPieModal = ({
12
- heading = 'This is a modal heading',
13
- headingLevel = 'h2',
14
- size = 'medium',
15
- isOpen = true,
16
- } : Partial<ModalProps> = {}) => `<pie-modal ${isOpen ? 'isOpen' : ''} heading="${heading}" headingLevel="${headingLevel}" size="${size}"></pie-modal>`;
17
-
18
- // Creates a <ol> with a large number of <li> nodes for testing page scrolling
19
- const createTestPageHTML = () => `<ol>
20
- ${'<li>List item</li>'.repeat(200)}
21
- </ol>`;
9
+ import { createScrollablePageHTML, renderTestPieModal } from '../helpers/index.ts';
22
10
 
23
11
  // Mount any components that are used inside of pie-modal so that
24
12
  // they have been registered with the browser before the tests run.
@@ -48,7 +36,7 @@ test('Should not be able to scroll when modal is open', async ({ page, mount })
48
36
  },
49
37
  slots: {
50
38
  component: modalComponent,
51
- pageMarkup: createTestPageHTML(),
39
+ pageMarkup: createScrollablePageHTML(),
52
40
  },
53
41
  },
54
42
  );
@@ -65,9 +53,7 @@ test('should not render when isOpen = false', async ({ page, mount }) => {
65
53
  await mount(PieModal, {
66
54
  props: {
67
55
  heading: 'This is a modal heading',
68
- headingLevel: 'h2',
69
56
  isOpen: false,
70
- size: 'medium',
71
57
  },
72
58
  });
73
59
 
@@ -79,7 +65,6 @@ sizes.forEach((size) => {
79
65
  await mount(PieModal, {
80
66
  props: {
81
67
  heading: 'This is a modal heading',
82
- headingLevel: 'h2',
83
68
  isOpen: true,
84
69
  size,
85
70
  },
@@ -88,3 +73,81 @@ sizes.forEach((size) => {
88
73
  await percySnapshot(page, `Modal - size = ${size}`);
89
74
  });
90
75
  });
76
+
77
+ test.describe('`isFullWidthBelowMid`', () => {
78
+ test.describe('when true', () => {
79
+ test('should be full width for a modal with size = medium', async ({ page, mount }) => {
80
+ await mount(PieModal, {
81
+ props: {
82
+ heading: 'This is a modal heading',
83
+ isFullWidthBelowMid: true,
84
+ isOpen: true,
85
+ size: 'medium',
86
+ },
87
+ });
88
+
89
+ await percySnapshot(page, 'Modal - isFullWidthBelowMid = true, size = medium');
90
+ });
91
+
92
+ test('should not be full width when size = small', async ({ page, mount }) => {
93
+ await mount(PieModal, {
94
+ props: {
95
+ heading: 'This is a modal heading',
96
+ isFullWidthBelowMid: true,
97
+ isOpen: true,
98
+ size: 'small',
99
+ },
100
+ });
101
+
102
+ await percySnapshot(page, 'Modal - isFullWidthBelowMid = true, size = small');
103
+ });
104
+ });
105
+
106
+ test.describe('when false', () => {
107
+ (['small', 'medium'] as Array<ModalProps['size']>)
108
+ .forEach((size) => {
109
+ test(`should not be full width for a modal with size = ${size}`, async ({ page, mount }) => {
110
+ await mount(PieModal, {
111
+ props: {
112
+ heading: 'This is a modal heading',
113
+ isFullWidthBelowMid: false,
114
+ isOpen: true,
115
+ size,
116
+ },
117
+ });
118
+
119
+ await percySnapshot(page, `Modal - isFullWidthBelowMid = false, size = ${size}`);
120
+ });
121
+ });
122
+ });
123
+ });
124
+
125
+ test.describe('`isDismissible`', () => {
126
+ test.describe('when true', () => {
127
+ test('should display a close button within the modal', async ({ mount, page }) => {
128
+ await mount(PieModal, {
129
+ props: {
130
+ heading: 'This is a modal heading',
131
+ isDismissible: true,
132
+ isOpen: true,
133
+ },
134
+ });
135
+
136
+ await percySnapshot(page, 'Modal with close button displayed - isDismissible: `true`');
137
+ });
138
+ });
139
+
140
+ test.describe('when false', () => {
141
+ test('should NOT display a close button', async ({ mount, page }) => {
142
+ await mount(PieModal, {
143
+ props: {
144
+ heading: 'This is a modal heading',
145
+ isDismissible: false,
146
+ isOpen: true,
147
+ },
148
+ });
149
+
150
+ await percySnapshot(page, 'Modal without close button - isDismissible: `false`');
151
+ });
152
+ });
153
+ });