10
10
'use strict' ;
11
11
12
12
import { clearDOM } from '@symfony/stimulus-testing' ;
13
- import { startStimulus } from '../tools' ;
14
- import { createEvent , fireEvent , getByLabelText , getByText , waitFor } from '@testing-library/dom' ;
13
+ import { initLiveComponent , mockRerender , startStimulus } from '../tools' ;
14
+ import { getByLabelText , getByText , waitFor } from '@testing-library/dom' ;
15
15
import userEvent from '@testing-library/user-event' ;
16
16
import fetchMock from 'fetch-mock-jest' ;
17
17
18
18
describe ( 'LiveController parent -> child component tests' , ( ) => {
19
19
const parentTemplate = ( data ) => {
20
+ const errors = data . errors || { post : { } } ;
21
+
20
22
return `
21
23
<div
22
- data-controller="live"
23
- data-live-url-value="http://localhost/_components/parent"
24
+ ${ initLiveComponent ( '/_components/parent' , data ) }
24
25
>
25
26
<span>Title: ${ data . post . title } </span>
26
27
<span>Description in Parent: ${ data . post . content } </span>
27
28
28
- <input
29
- type="text"
30
- name="post[title]"
31
- value="${ data . post . title } "
32
- >
29
+ <label>
30
+ Title:
31
+ <input
32
+ type="text"
33
+ name="post[title]"
34
+ value="${ data . post . title } "
35
+ data-action="live#update"
36
+ >
37
+ </label>
33
38
34
- ${ childTemplate ( { value : data . post . content } ) }
39
+ ${ childTemplate ( { value : data . post . content , error : errors . post . content } ) }
35
40
36
41
<button
37
42
data-action="live#$render"
@@ -42,82 +47,66 @@ describe('LiveController parent -> child component tests', () => {
42
47
43
48
const childTemplate = ( data ) => `
44
49
<div
45
- data-controller="live"
46
- data-live-url-value="http://localhost/_components/child"
50
+ ${ initLiveComponent ( '/_components/child' , data ) }
47
51
>
48
- <!-- form field not mapped with data-model -->
49
52
<label>
50
53
Content:
51
- <input
52
- value="${ data . value } "
54
+ <textarea
53
55
data-model="value"
54
56
name="post[content]"
55
57
data-action="live#update"
56
- >
58
+ rows="${ data . rows ? data . rows : '3' } "
59
+ >${ data . value } </textarea>
57
60
</label>
58
61
59
62
<div>Value in child: ${ data . value } </div>
63
+ <div>Error in child: ${ data . error ? data . error : 'none' } </div>
64
+ {# Rows represents a writable prop that's private to the child component #}
65
+ <div>Rows in child: ${ data . rows ? data . rows : 'not set' } </div>
60
66
61
67
<button
62
68
data-action="live#$render"
63
69
>Child Re-render</button>
64
70
</div>
65
71
` ;
66
72
67
- const startStimulusForParentChild = async ( html , data , childData ) => {
68
- const testData = await startStimulus (
69
- parentTemplate ( data ) ,
70
- data
71
- ) ;
72
-
73
- // setup the values on the child element
74
- testData . element . querySelector ( '[data-controller="live"]' ) . dataset . liveDataValue = JSON . stringify ( childData ) ;
75
-
76
- return testData ;
77
- }
78
-
79
73
afterEach ( ( ) => {
80
74
clearDOM ( ) ;
81
75
fetchMock . reset ( ) ;
82
76
} ) ;
83
77
84
78
it ( 'renders parent component without affecting child component' , async ( ) => {
85
- const data = { post : { title : 'Parent component' , content : 'i love components' } } ;
86
- const { element } = await startStimulusForParentChild (
87
- parentTemplate ( data ) ,
88
- data ,
89
- { value : data . post . content }
90
- ) ;
91
-
92
- // on child re-render, use a new content
93
- fetchMock . get ( 'http://localhost/_components/child?value=i+love+components' , {
94
- html : childTemplate ( { value : 'i love popcorn' } ) ,
95
- data : { value : 'i love popcorn' }
79
+ const data = { post : { title : 'Parent component' , content : 'i love' } } ;
80
+ const { element } = await startStimulus ( parentTemplate ( data ) ) ;
81
+
82
+ // on child re-render, expect the new value, change rows on the server
83
+ mockRerender ( { value : 'i love popcorn' } , childTemplate , ( data ) => {
84
+ // change the "rows" data on the "server"
85
+ data . rows = 5 ;
96
86
} ) ;
87
+ await userEvent . type ( getByLabelText ( element , 'Content:' ) , ' popcorn' ) ;
97
88
98
- // reload the child template
99
- getByText ( element , 'Child Re-render' ) . click ( ) ;
100
89
await waitFor ( ( ) => expect ( element ) . toHaveTextContent ( 'Value in child: i love popcorn' ) ) ;
101
-
102
- // on parent re-render, render the child template differently
103
- fetchMock . get ( 'begin:http://localhost/_components/parent' , {
104
- html : parentTemplate ( { post : { title : 'Changed title' , content : 'changed content' } } ) ,
105
- data : { post : { title : 'Changed title' , content : 'changed content' } }
106
- } ) ;
107
- getByText ( element , 'Parent Re-render' ) . click ( ) ;
108
- await waitFor ( ( ) => expect ( element ) . toHaveTextContent ( 'Title: Changed title' ) ) ;
90
+ expect ( element ) . toHaveTextContent ( 'Rows in child: 5' ) ;
91
+
92
+ // when the parent re-renders, expect the changed title AND content (from child)
93
+ // but, importantly, the only "changed" data that will be passed into
94
+ // the child component will be "content", which will match what the
95
+ // child already has. This will NOT trigger a re-render.
96
+ mockRerender (
97
+ { post : { title : 'Parent component changed' , content : 'i love popcorn' } } ,
98
+ parentTemplate
99
+ )
100
+ await userEvent . type ( getByLabelText ( element , 'Title:' ) , ' changed' ) ;
101
+ await waitFor ( ( ) => expect ( element ) . toHaveTextContent ( 'Title: Parent component changed' ) ) ;
109
102
110
103
// the child component should *not* have updated
111
- expect ( element ) . toHaveTextContent ( 'Value in child: i love popcorn ' ) ;
104
+ expect ( element ) . toHaveTextContent ( 'Rows in child: 5 ' ) ;
112
105
} ) ;
113
106
114
107
it ( 'updates child model and parent model in a deferred way' , async ( ) => {
115
108
const data = { post : { title : 'Parent component' , content : 'i love' } } ;
116
- const { element, controller } = await startStimulusForParentChild (
117
- parentTemplate ( data ) ,
118
- data ,
119
- { value : data . post . content }
120
- ) ;
109
+ const { element, controller } = await startStimulus ( parentTemplate ( data ) ) ;
121
110
122
111
// verify the child request contains the correct description & re-render
123
112
fetchMock . get ( 'http://localhost/_components/child?value=i+love+turtles' , {
@@ -139,6 +128,43 @@ describe('LiveController parent -> child component tests', () => {
139
128
expect ( controller . dataValue . post . content ) . toEqual ( 'i love turtles' ) ;
140
129
} ) ;
141
130
131
+ it ( 'updates re-renders a child component if data has changed from initial' , async ( ) => {
132
+ const data = { post : { title : 'Parent component' , content : 'initial content' } } ;
133
+ const { element } = await startStimulus ( parentTemplate ( data ) ) ;
134
+
135
+ // allow the child to re-render, but change the "rows" value
136
+ const inputElement = getByLabelText ( element , 'Content:' ) ;
137
+ await userEvent . clear ( inputElement ) ;
138
+ await userEvent . type ( inputElement , 'changed content' ) ;
139
+ fetchMock . get ( 'http://localhost/_components/child?value=changed+content' , {
140
+ html : childTemplate ( { value : 'changed content' , rows : 5 } ) ,
141
+ data : { value : 'changed content' , rows : 5 }
142
+ } ) ;
143
+
144
+ // reload, which will give us rows=5
145
+ getByText ( element , 'Child Re-render' ) . click ( ) ;
146
+ await waitFor ( ( ) => expect ( element ) . toHaveTextContent ( 'Rows in child: 5' ) ) ;
147
+
148
+ // simulate an action in the parent component where "errors" changes
149
+ const newData = { ...data } ;
150
+ newData . post . title = 'Changed title' ;
151
+ newData . post . content = 'changed content' ;
152
+ newData . errors = { post : { content : 'the content is not interesting enough' } } ;
153
+ fetchMock . get ( 'http://localhost/_components/parent?post%5Btitle%5D=Parent+component&post%5Bcontent%5D=changed+content' , {
154
+ html : parentTemplate ( newData ) ,
155
+ data : newData
156
+ } ) ;
157
+
158
+ getByText ( element , 'Parent Re-render' ) . click ( ) ;
159
+ await waitFor ( ( ) => expect ( element ) . toHaveTextContent ( 'Title: Changed title' ) ) ;
160
+ // the child, of course, still has the "changed content" value
161
+ expect ( element ) . toHaveTextContent ( 'Value in child: changed content' ) ;
162
+ // but because some child data *changed* from its original value, the child DOES re-render
163
+ expect ( element ) . toHaveTextContent ( 'Error in child: the content is not interesting enough' ) ;
164
+ // however, this means that the updated "rows" data on the child is lost
165
+ expect ( element ) . toHaveTextContent ( 'Rows in child: not set' ) ;
166
+ } ) ;
167
+
142
168
// TODO - what if a child component re-renders and comes down with
143
169
// a changed set of data? Should that update the parent's data?
144
170
} ) ;
0 commit comments