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

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.
@@ -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