@financial-times/n-myft-ui 24.0.1 → 25.0.0-beta.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,68 @@
1
+ import React from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+ import FollowButton from '../follow-button/follow-button';
4
+
5
+ export default function Collections ({ title, liteStyle, flags, collectionName, trackable, concepts = [] }) {
6
+ const getLiteStyleModifier = () => liteStyle ? 'lite' : 'regular';
7
+ let formProps = {
8
+ method: 'POST',
9
+ action: '#',
10
+ 'data-myft-ui': 'follow',
11
+ 'data-concept-id': concepts.map(concept => concept.id).join(',')
12
+ };
13
+
14
+ if (collectionName) {
15
+ formProps['data-myft-tracking'] = collectionName;
16
+ }
17
+
18
+ return (
19
+ <section
20
+ className={`collection collection--${getLiteStyleModifier()}`}
21
+ data-trackable={trackable ? trackable : 'collection'}>
22
+ <header className={`collection__header collection__header--${getLiteStyleModifier()}`}>
23
+ <h2 className={`collection__title collection__title--${getLiteStyleModifier()}`}>
24
+ {title}
25
+ </h2>
26
+ </header>
27
+ <ul className="collection__concepts">
28
+ {concepts && concepts.map((concept, index) =>
29
+ <li className="collection__concept" key={index}>
30
+ <FollowButton variant={liteStyle ? 'primary' : 'inverse'} buttonText={concept.name} flags={flags} collectionName={collectionName} />
31
+ </li>)
32
+ }
33
+ </ul>
34
+
35
+ <div className="collection__meta">
36
+ <form
37
+ {...formProps}
38
+ className="n-myft-ui n-myft-ui--follow n-ui-hide-core collection-follow-all">
39
+ <input
40
+ type="hidden"
41
+ name="directType"
42
+ value={concepts.map(concept => concept.directType).join(',')}
43
+ />
44
+ <CsrfToken />
45
+ <input
46
+ type="hidden"
47
+ name="name"
48
+ value={concepts.map(concept => concept.name).join(',')}
49
+ />
50
+ <button
51
+ type="submit"
52
+ aria-pressed="false"
53
+ className={`collection-follow-all__button collection-follow-all__button--${getLiteStyleModifier()}`}
54
+ data-trackable="follow all"
55
+ data-concept-id={concepts.map(concept => concept.id).join(',')}
56
+ aria-label={`Add all topics in the ${title} collection to my F T`}
57
+ data-alternate-label={`Remove all topics in the ${title} collection from my F T`}
58
+ data-alternate-text="Added"
59
+ title={`Add all topics in the ${title} collection to my F T`}>
60
+ Add all to myFT
61
+ </button>
62
+ </form>
63
+ </div>
64
+ </section>
65
+
66
+ )
67
+
68
+ }
@@ -0,0 +1,83 @@
1
+ import React from 'react';
2
+ import Collections from './collections';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const fixtures = {
7
+ 'title': 'European Union',
8
+ 'concepts': [
9
+ {
10
+ 'id': '00000000-0000-0000-0000-000000000000',
11
+ 'prefLabel': 'EU immigration',
12
+ 'directType': 'http://www.ft.com/ontology/Topic',
13
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
14
+ 'name': 'EU immigration'
15
+ },
16
+ {
17
+ 'id': '00000000-0000-0000-0000-000000000001',
18
+ 'prefLabel': 'Europe Quantitative Easing',
19
+ 'directType': 'http://www.ft.com/ontology/Topic',
20
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
21
+ 'name': 'Europe Quantitative Easing'
22
+ },
23
+ {
24
+ 'id': '00000000-0000-0000-0000-000000000002',
25
+ 'prefLabel': 'EU financial regulation',
26
+ 'directType': 'http://www.ft.com/ontology/Topic',
27
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
28
+ 'name': 'EU financial regulation'
29
+ },
30
+ {
31
+ 'id': '00000000-0000-0000-0000-000000000003',
32
+ 'prefLabel': 'EU nothing',
33
+ 'directType': 'http://www.ft.com/ontology/Topic',
34
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
35
+ 'name': 'EU nothing'
36
+ },
37
+ {
38
+ 'id': '00000000-0000-0000-0000-000000000004',
39
+ 'prefLabel': 'EU trade',
40
+ 'directType': 'http://www.ft.com/ontology/Topic',
41
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000000',
42
+ 'name': 'EU trade'
43
+ }
44
+ ]
45
+ };
46
+ const joinedDirectTypes = 'http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic,http://www.ft.com/ontology/Topic';
47
+
48
+ const flags = {
49
+ myFtApi: true,
50
+ myFtApiWrite: true
51
+ };
52
+
53
+ describe('Concept List', () => {
54
+
55
+ test('It renders the title of the collection', async () => {
56
+ render(<Collections {...fixtures} flags={flags} />);
57
+ expect(await screen.findByText('European Union')).toBeTruthy();
58
+ });
59
+
60
+ test('It renders label for the concept button', async () => {
61
+ render(<Collections {...fixtures} flags={flags} />);
62
+ expect(await screen.findByText('EU immigration')).toBeTruthy();
63
+ expect(await screen.findByText('Europe Quantitative Easing')).toBeTruthy();
64
+ expect(await screen.findByText('EU financial regulation')).toBeTruthy();
65
+ expect(await screen.findByText('EU nothing')).toBeTruthy();
66
+ expect(await screen.findByText('EU trade')).toBeTruthy();
67
+ });
68
+
69
+ test('It renders form "Add all to my FT" from', () => {
70
+ const { container} = render(<Collections {...fixtures} flags={flags} />);
71
+ const formElement = container.querySelector('form[action="#"]');
72
+ expect(formElement).toBeTruthy();
73
+ expect(formElement.method).toEqual('post');
74
+ });
75
+
76
+ test('It renders directType input with value of types joined', () => {
77
+ const { container} = render(<Collections {...fixtures} flags={flags} />);
78
+ const directTypeElement = container.querySelector('input[name="directType"]');
79
+ expect(directTypeElement).toBeTruthy();
80
+ expect(directTypeElement.value).toEqual(joinedDirectTypes);
81
+ });
82
+
83
+ });
@@ -0,0 +1,63 @@
1
+ import React, { Fragment } from 'react';
2
+ import FollowButton from '../follow-button/follow-button';
3
+
4
+ export default function ConceptList ({ flags, concepts, contentType, conceptListTitle, trackable }) {
5
+
6
+ const {
7
+ myFtApi,
8
+ myFtApiWrite
9
+ } = flags;
10
+
11
+ const generateTrackableProps = (primary, seconday) => {
12
+ return {
13
+ 'data-trackable': primary ? primary : seconday
14
+ }
15
+ }
16
+
17
+ const shouldDisplay = () => {
18
+ if(myFtApi && myFtApiWrite && Array.isArray(concepts) && concepts.length) {
19
+ return true
20
+ }
21
+
22
+ return false;
23
+ }
24
+
25
+
26
+ return (
27
+
28
+ <Fragment>
29
+ {shouldDisplay() &&
30
+ <div
31
+ className='concept-list'
32
+ {...generateTrackableProps(trackable, 'concept-list')}>
33
+ {
34
+ (contentType || conceptListTitle) &&
35
+ <h2 className='concept-list__title'>
36
+ {conceptListTitle ? conceptListTitle : `Follow the topics in this ${contentType}`}
37
+ </h2>
38
+ }
39
+ <ul className='concept-list__list'>
40
+ {concepts.map((concept, index) => {
41
+ const {
42
+ relativeUrl,
43
+ url,
44
+ conceptTrackable,
45
+ prefLabel,
46
+ id
47
+ } = concept;
48
+ return (
49
+ <li key={index} className='concept-list__list-item'>
50
+ <a
51
+ href={relativeUrl || url}
52
+ {...generateTrackableProps(conceptTrackable, 'concept')}
53
+ className='concept-list__concept'>
54
+ {prefLabel}
55
+ </a>
56
+ <FollowButton conceptId={id} name={prefLabel} flags={flags} />
57
+ </li>
58
+ )
59
+ })}
60
+ </ul>
61
+ </div>}
62
+ </Fragment>)
63
+ }
@@ -0,0 +1,116 @@
1
+ import React from 'react';
2
+ import ConceptList from './concept-list';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const fixtures = [
7
+ {
8
+ 'conceptListTitle': 'Follow european union things',
9
+ 'concepts': [
10
+ {
11
+ 'id': '00000000-0000-0000-0000-000000000161',
12
+ 'prefLabel': 'EU immigration',
13
+ 'directType': 'http://www.ft.com/ontology/Topic',
14
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000161',
15
+ 'name': 'EU immigration'
16
+ },
17
+ {
18
+ 'id': '00000000-0000-0000-0000-000000000162',
19
+ 'prefLabel': 'Europe Quantitative Easing',
20
+ 'directType': 'http://www.ft.com/ontology/Topic',
21
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000162',
22
+ 'name': 'Europe Quantitative Easing'
23
+ },
24
+ {
25
+ 'id': '00000000-0000-0000-0000-000000000163',
26
+ 'prefLabel': 'EU financial regulation',
27
+ 'directType': 'http://www.ft.com/ontology/Topic',
28
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000163',
29
+ 'name': 'EU financial regulation'
30
+ },
31
+ {
32
+ 'id': '00000000-0000-0000-0000-000000000164',
33
+ 'prefLabel': 'EU nothing',
34
+ 'directType': 'http://www.ft.com/ontology/Topic',
35
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000164',
36
+ 'name': 'EU nothing'
37
+ },
38
+ {
39
+ 'id': '00000000-0000-0000-0000-000000000165',
40
+ 'prefLabel': 'EU trade',
41
+ 'directType': 'http://www.ft.com/ontology/Topic',
42
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000165',
43
+ 'name': 'EU trade'
44
+ }
45
+ ]
46
+ },
47
+ {
48
+ 'contentType': 'search',
49
+ 'concepts': [
50
+ {
51
+ 'id': '00000000-0000-0000-0000-000000000166',
52
+ 'prefLabel': 'Noodle',
53
+ 'directType': 'http://www.ft.com/ontology/Topic',
54
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000166',
55
+ 'name': 'Noodle'
56
+ },
57
+ {
58
+ 'id': '00000000-0000-0000-0000-000000000167',
59
+ 'prefLabel': 'Green apples',
60
+ 'directType': 'http://www.ft.com/ontology/Topic',
61
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000167',
62
+ 'name': 'Green apples'
63
+ },
64
+ {
65
+ 'id': '00000000-0000-0000-0000-000000000168',
66
+ 'prefLabel': 'Fox blood',
67
+ 'directType': 'http://www.ft.com/ontology/Topic',
68
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000168',
69
+ 'name': 'Fox blood'
70
+ },
71
+ {
72
+ 'id': '00000000-0000-0000-0000-000000000169',
73
+ 'prefLabel': 'Dog party',
74
+ 'directType': 'http://www.ft.com/ontology/Topic',
75
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000169',
76
+ 'name': 'Dog party'
77
+ },
78
+ {
79
+ 'id': '00000000-0000-0000-0000-000000000170',
80
+ 'prefLabel': 'Fifth thing',
81
+ 'directType': 'http://www.ft.com/ontology/Topic',
82
+ 'url': 'https://www.ft.com/stream/00000000-0000-0000-0000-000000000170',
83
+ 'name': 'Fifth thing'
84
+ }
85
+ ]
86
+ },
87
+ ];
88
+
89
+
90
+ const flags = {
91
+ myFtApi: true,
92
+ myFtApiWrite: true
93
+ };
94
+
95
+ describe('Concept List', () => {
96
+
97
+ test('It renders conceptListTitle value as title when conceptListTitle is provided', async () => {
98
+ render(<ConceptList {...fixtures[0]} flags={flags} />);
99
+ expect(await screen.findByText('Follow european union things')).toBeTruthy();
100
+ });
101
+
102
+ test('It renders "Follow the topics in this {conceptType}" value as title when conceptType is provided', async () => {
103
+ render(<ConceptList {...fixtures[1]} flags={flags} />);
104
+ expect(await screen.findByText('Follow the topics in this search')).toBeTruthy();
105
+ });
106
+
107
+ test('It renders label for the concept button', async () => {
108
+ render(<ConceptList {...fixtures[0]} flags={flags} />);
109
+ expect(await screen.findByText('EU immigration')).toBeTruthy();
110
+ expect(await screen.findByText('Europe Quantitative Easing')).toBeTruthy();
111
+ expect(await screen.findByText('EU financial regulation')).toBeTruthy();
112
+ expect(await screen.findByText('EU nothing')).toBeTruthy();
113
+ expect(await screen.findByText('EU trade')).toBeTruthy();
114
+ });
115
+
116
+ });
@@ -16,7 +16,7 @@ describe('Follow button', () => {
16
16
 
17
17
  test('It renders default button', async () => {
18
18
  render(<FollowButton {...props} />);
19
- expect(screen.findByText('Add to myFT')).toBeTruthy();
19
+ expect(await screen.findByText('Add to myFT')).toBeTruthy();
20
20
  });
21
21
 
22
22
  test('It renders a variant', async () => {
@@ -29,12 +29,12 @@ describe('Follow button', () => {
29
29
  expect(container.querySelector(`form[action='/myft/add/${props.conceptId}']`)).toBeTruthy();
30
30
  });
31
31
 
32
- test('Button state changes when attributes change', () => {
32
+ test('Button state changes when attributes change', async () => {
33
33
  render(<FollowButton {...props}
34
34
  variant={'standard'}
35
35
  setFollowButtonStateToSelected={true}
36
36
  cacheablePersonalisedUrl={true} />);
37
- expect(screen.findByText('Added')).toBeTruthy();
37
+ expect(await screen.findByText('Added')).toBeTruthy();
38
38
  });
39
39
 
40
40
  });
@@ -1,4 +1,4 @@
1
- import React from 'react';
1
+ import React, {Fragment} from 'react';
2
2
  import CsrfToken from '../csrf-token/input';
3
3
 
4
4
  function generateFormProps (props) {
@@ -148,7 +148,7 @@ export default function FollowButton (props) {
148
148
  const getVariantClass = (variant) => variant ? `n-myft-follow-button--${variant}` : '';
149
149
 
150
150
  return (
151
- <>
151
+ <Fragment>
152
152
  {flags.myFtApiWrite && <form
153
153
  className={`n-myft-ui n-myft-ui--follow ${extraClasses || ''}`}
154
154
  method="GET"
@@ -168,7 +168,7 @@ export default function FollowButton (props) {
168
168
  {getButtonText(props)}
169
169
  </button>
170
170
  </form>}
171
- </>
171
+ </Fragment>
172
172
  );
173
173
 
174
174
  }
@@ -0,0 +1,15 @@
1
+ import CsrfToken from './csrf-token/input';
2
+ import FollowButton from './follow-button/follow-button';
3
+ import ConceptList from './concept-list/concept-list';
4
+ import Collections from './collections/collections';
5
+ import SaveForLater from './save-for-later/save-for-later';
6
+ import PinButton from './pin-button/pin-button';
7
+
8
+ export {
9
+ CsrfToken,
10
+ FollowButton,
11
+ ConceptList,
12
+ Collections,
13
+ SaveForLater,
14
+ PinButton
15
+ };
@@ -0,0 +1,40 @@
1
+ import React, { Fragment } from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+ export default function PinButton ({ showPrioritiseButton, id, name, directType, prioritised }) {
4
+
5
+ const getAction = () => `/__myft/api/core/prioritised/concept/${id}?method=${prioritised ? 'delete' : 'put'}`
6
+
7
+ return (
8
+ <Fragment>
9
+ {showPrioritiseButton &&
10
+ <Fragment>
11
+ <span className="myft-pin-divider"></span>
12
+ <div className="myft-pin-button-wrapper">
13
+ <form method="post" action={getAction()} data-myft-prioritise>
14
+ <CsrfToken />
15
+ <input type="hidden" value={name} name="name" />
16
+ <input type="hidden" value={directType || 'http://www.ft.com/ontology/concept/Concept'} name="directType" />
17
+ <div
18
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
19
+ aria-live="assertive"
20
+ data-pressed-text={`${name} pinned in myFT.`}
21
+ data-unpressed-text={`Unpinned ${name} from myFT.`}
22
+ ></div>
23
+ <button id={`myft-pin-button__${id}`}
24
+ className="myft-pin-button"
25
+ data-prioritise-button
26
+ data-trackable="prioritised"
27
+ data-concept-id={id}
28
+ data-prioritised={prioritised ? true : false}
29
+ aria-label={`${prioritised ? 'Unpin' : 'Pin'} ${name} ${prioritised ? 'from' : 'in'} myFT`}
30
+ aria-pressed={prioritised ? true : false}
31
+ title={`${prioritised ? 'Unpin' : 'Pin'} ${name}`}>
32
+ </button>
33
+ </form>
34
+ </div>
35
+ </Fragment>
36
+ }
37
+ </Fragment>
38
+ )
39
+
40
+ }
@@ -0,0 +1,57 @@
1
+ import React from 'react';
2
+ import { render } from '@testing-library/react';
3
+ import '@testing-library/jest-dom';
4
+ import PinButton from './pin-button';
5
+
6
+ const flags = {
7
+ myFtApi: true,
8
+ myFtApiWrite: true
9
+ };
10
+
11
+ const fixtures = [
12
+ {
13
+ id: '00000000-0000-0000-0000-000000000022',
14
+ name: 'myFT Enterprises',
15
+ directType: 'http://www.ft.com/ontology/Topic',
16
+ showPrioritiseButton: true
17
+ },
18
+ {
19
+ id: '00000000-0000-0000-0000-000000000023',
20
+ name: 'myFT Enterprises',
21
+ directType: 'http://www.ft.com/ontology/Topic',
22
+ showPrioritiseButton: false
23
+ }
24
+ ];
25
+
26
+
27
+ describe('Pin Button', () => {
28
+
29
+ test('It renders', () => {
30
+ const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
31
+ expect(container.querySelector(`button[id="myft-pin-button__${fixtures[0].id}"]`)).toBeTruthy();
32
+ expect(container.querySelector('button[data-trackable="prioritised"]')).toBeTruthy();
33
+ });
34
+
35
+ test('It renders unprioritised', () => {
36
+ const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
37
+ expect(container.querySelector('button[aria-label="Pin myFT Enterprises in myFT"]')).toBeTruthy();
38
+ expect(container.querySelector('button[title="Pin myFT Enterprises"]')).toBeTruthy();
39
+ expect(container.querySelector('button[data-prioritised=false]')).toBeTruthy();
40
+ expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
41
+ });
42
+
43
+ test('It renders with prioritised', () => {
44
+ const { container } = render(<PinButton flags={flags} prioritised={true} {...fixtures[0]} />);
45
+ expect(container.querySelector('button[aria-label="Unpin myFT Enterprises from myFT"]')).toBeTruthy();
46
+ expect(container.querySelector('button[title="Unpin myFT Enterprises"]')).toBeTruthy();
47
+ expect(container.querySelector('button[data-prioritised=true]')).toBeTruthy();
48
+ expect(container.querySelector(`button[data-concept-id="${fixtures[0].id}"]`)).toBeTruthy();
49
+ });
50
+
51
+ test('It renders the form element', () => {
52
+ const { container } = render(<PinButton flags={flags} {...fixtures[0]} />);
53
+ expect(container.querySelector('form[method="post"]')).toBeTruthy();
54
+ expect(container.querySelector(`form[action="/__myft/api/core/prioritised/concept/${fixtures[0].id}?method=put"]`)).toBeTruthy();
55
+ });
56
+
57
+ });
@@ -0,0 +1,103 @@
1
+ import React, { Fragment } from 'react';
2
+ import CsrfToken from '../csrf-token/input';
3
+
4
+ const ButtonContent = ({ saveButtonWithIcon, buttonText, isSaved, appIsStreamPage }) => {
5
+
6
+ return (<Fragment>
7
+ {
8
+ saveButtonWithIcon &&
9
+ <span className="save-button-with-icon-copy" data-variant-label>
10
+ {buttonText && buttonText}
11
+ {!buttonText && (isSaved ? 'Saved' : 'Save')}
12
+ </span>
13
+ }
14
+
15
+ {
16
+ !saveButtonWithIcon &&
17
+ <Fragment>
18
+ {buttonText && buttonText}
19
+ {!buttonText &&
20
+ <Fragment>
21
+ {
22
+ appIsStreamPage !== true &&
23
+ <Fragment>
24
+ <span className="save-button-longer-copy" data-variant-label>
25
+ {isSaved ? 'Saved ' : 'Save '}
26
+ </span>
27
+ <span className="n-myft-ui__button--viewport-large" aria-hidden="true">to myFT</span>
28
+ </Fragment>
29
+ }
30
+
31
+ {
32
+ appIsStreamPage === true && <span>{isSaved ? 'Saved' : 'Save'}</span>
33
+ }
34
+ </Fragment>
35
+ }
36
+ </Fragment>
37
+ }
38
+ </Fragment>);
39
+ }
40
+ export default function SaveForLater ({ flags, contentId, title, variant, trackableId, isSaved, appIsStreamPage, alternateText, saveButtonWithIcon, buttonText }) {
41
+
42
+ const { myFtApiWrite } = flags;
43
+
44
+ const generateSubmitButtonProps = () => {
45
+ let props = {
46
+ type: 'submit',
47
+ 'data-trackable': trackableId ? trackableId : 'save-for-later',
48
+ 'data-text-variant': appIsStreamPage !== true ? 'save-button-with-icon-copy' : 'save-button-longer-copy',
49
+ 'data-content-id': contentId,
50
+ className: saveButtonWithIcon ? 'n-myft-ui__save-button-with-icon' : `n-myft-ui__button ${variant ? `n-myft-ui__button--${variant}` : ''}`
51
+ };
52
+
53
+ if (isSaved) {
54
+ let titleText = `${title ? `${title} is` : ''} Saved to myFT`;
55
+ props['title'] = title;
56
+ props['aria-label'] = titleText;
57
+ props['data-alternate-label'] = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
58
+ props['aria-pressed'] = true;
59
+ } else {
60
+ let titleText = title ? `Save ${title} to myFT for later` : 'Save this article to myFT for later';
61
+ props['title'] = titleText;
62
+ props['aria-label'] = titleText;
63
+ props['data-alternate-label'] = `${title ? `${title} is` : ''} Saved to myFT`;
64
+ props['aria-pressed'] = false;
65
+ }
66
+
67
+ if (alternateText) {
68
+ props['data-alternate-text'] = alternateText;
69
+ } else if (isSaved) {
70
+ props['data-alternate-text'] = 'Save ';
71
+ } else {
72
+ props['data-alternate-text'] = 'Saved ';
73
+ }
74
+
75
+ return props;
76
+ }
77
+
78
+
79
+ return (
80
+ <Fragment>
81
+ {myFtApiWrite &&
82
+ <form className="n-myft-ui n-myft-ui--save" method="GET"
83
+ data-content-id={contentId}
84
+ data-myft-ui="saved"
85
+ action={`/myft/save/${contentId}`}
86
+ data-js-action={`/__myft/api/core/saved/content/${contentId}?method=put`}>
87
+ <CsrfToken />
88
+
89
+ <div
90
+ className="n-myft-ui__announcement o-normalise-visually-hidden"
91
+ aria-live="assertive"
92
+ data-pressed-text="Article saved in My FT."
93
+ data-unpressed-text="Removed article from My FT."
94
+ ></div>
95
+ <button {...generateSubmitButtonProps()}>
96
+ <ButtonContent buttonText={buttonText} saveButtonWithIcon={saveButtonWithIcon} isSaved={isSaved} appIsStreamPage={appIsStreamPage}/>
97
+ </button>
98
+ </form>
99
+ }
100
+ </Fragment>
101
+
102
+ )
103
+ }
@@ -0,0 +1,59 @@
1
+ import React from 'react';
2
+ import SaveForLater from './save-for-later';
3
+ import { render, screen } from '@testing-library/react';
4
+ import '@testing-library/jest-dom';
5
+
6
+ const flags = {
7
+ myFtApi: true,
8
+ myFtApiWrite: true
9
+ };
10
+
11
+ const fixture = {
12
+ contentId: '00000000-0000-0000-0000-000000000033',
13
+ title: 'myFT Enterprises'
14
+ };
15
+
16
+ describe('SaveForLater component', () => {
17
+
18
+ test('It renders', async () => {
19
+ render(<SaveForLater flags={flags} {...fixture}/>);
20
+ expect(await screen.findByText('Save')).toBeTruthy();
21
+ });
22
+
23
+ test('It renders button text wen provided', async () => {
24
+ render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
25
+ expect(await screen.findByText('Globetrotter')).toBeTruthy();
26
+ });
27
+
28
+ test('It renders the correct form action attribute', () => {
29
+ const { container } = render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
30
+ const formElement = container.querySelector(`form[action="/myft/save/${fixture.contentId}"]`);
31
+ expect(formElement).toBeTruthy();
32
+ });
33
+
34
+ test('It renders the correct form data-js-action attribute', () => {
35
+ const { container } = render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
36
+ const formElement = container.querySelector(`form[data-js-action="/__myft/api/core/saved/content/${fixture.contentId}?method=put"]`);
37
+ expect(formElement).toBeTruthy();
38
+ });
39
+
40
+ test('It renders the correct form method attribute', () => {
41
+ const { container } = render(<SaveForLater flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
42
+ const formElement = container.querySelector('form[method="GET"]');
43
+ expect(formElement).toBeTruthy();
44
+ });
45
+
46
+ test('It renders the correct button data-text-variant attribute when appIsStreamPage=true', () => {
47
+ const { container } = render(<SaveForLater appIsStreamPage={true} flags={flags} {...fixture} buttonText={'Globetrotter'}/>);
48
+ const buttonElement = container.querySelector('button[data-text-variant="save-button-longer-copy"]');
49
+ expect(buttonElement).toBeTruthy();
50
+ });
51
+
52
+ describe('Saved', () => {
53
+ test('It renders saved item', async () => {
54
+ render(<SaveForLater isSaved={true} flags={flags} {...fixture}/>);
55
+ expect(await screen.findByText('Saved')).toBeTruthy();
56
+ });
57
+ });
58
+
59
+ });
package/demos/app.js CHANGED
@@ -41,9 +41,7 @@ app.set('view engine', '.html');
41
41
  app.engine('.html', new PageKitHandlebars({
42
42
  cache: false,
43
43
  handlebars,
44
- helpers: {
45
- ...helpers
46
- }
44
+ helpers
47
45
  }).engine);
48
46
 
49
47
  app.use('/public', nExpress.static(path.join(__dirname, '../public'), { redirect: false }));
@@ -56,7 +54,7 @@ app.get('/', (req, res) => {
56
54
  flags: {
57
55
  myFtApi: true,
58
56
  myFtApiWrite: true
59
- }
57
+ },
60
58
  }, fixtures));
61
59
  });
62
60