@financial-times/cp-content-pipeline-schema 3.7.0 → 3.7.2

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,10 @@
1
1
  import { CAPIImage } from './Image'
2
- import logger from '@dotcom-reliability-kit/logger'
2
+ import { jest } from '@jest/globals'
3
3
  import type { QueryContext } from '..'
4
4
  import { Image } from './schemas/capi/internal-content'
5
+ import { OrigamiImageDataSource } from '../datasources/origami-image'
5
6
  import { MAX_IMAGE_WIDTH } from '../helpers/imageService'
7
+ import type { ImageSource } from '../generated'
6
8
 
7
9
  describe('Image', () => {
8
10
  const mockImage = {
@@ -11,8 +13,6 @@ describe('Image', () => {
11
13
  title: 'title',
12
14
  description: 'description',
13
15
  binaryUrl: 'cloudfront.com/image',
14
- pixelWidth: 5000,
15
- pixelHeight: 1000,
16
16
  }
17
17
 
18
18
  const graphic = {
@@ -20,17 +20,34 @@ describe('Image', () => {
20
20
  type: 'http://www.ft.com/ontology/content/Graphic' as const,
21
21
  }
22
22
 
23
+ const getImageMetadata = jest.fn<OrigamiImageDataSource['getImageMetadata']>()
24
+
23
25
  const context = {
24
- logger,
26
+ dataSources: {
27
+ origami: {
28
+ getImageMetadata,
29
+ },
30
+ capi: {},
31
+ vanityUrls: {},
32
+ },
25
33
  systemCode: 'image-test',
26
34
  } as unknown as QueryContext
27
35
 
36
+ beforeEach(() => {
37
+ getImageMetadata.mockReset()
38
+ })
39
+
28
40
  describe('sourceSet', () => {
29
41
  describe('An image that is provided with a high resolution', () => {
30
- it('transforms the URL to use the Origami Image Service', async () => {
42
+ let sourceSet: ImageSource[]
43
+
44
+ beforeAll(async () => {
45
+ getImageMetadata.mockResolvedValue({ width: 5000, height: 1000 })
31
46
  const model = new CAPIImage(mockImage, context)
32
- const sourceSet = await model.sourceSet({ width: 1000 })
47
+ sourceSet = await model.sourceSet({ width: 1000 })
48
+ })
33
49
 
50
+ it('transforms the URL to use the Origami Image Service', () => {
34
51
  expect(
35
52
  sourceSet.every((source) =>
36
53
  source.url.startsWith(
@@ -40,10 +57,7 @@ describe('Image', () => {
40
57
  ).toBeTruthy()
41
58
  })
42
59
 
43
- it('resizes the image to the requested width', async () => {
44
- const model = new CAPIImage(mockImage, context)
45
- const sourceSet = await model.sourceSet({ width: 1000 })
46
-
60
+ it('resizes the image to the requested width', () => {
47
61
  expect(
48
62
  sourceSet.every(
49
63
  (source) =>
@@ -52,9 +66,7 @@ describe('Image', () => {
52
66
  ).toBeTruthy()
53
67
  })
54
68
 
55
- it('includes sourceSet for all the possible resolutions we can display the image at, given the requested width and the original source width', async () => {
56
- const model = new CAPIImage(mockImage, context)
57
- const sourceSet = await model.sourceSet({ width: 1000 })
69
+ it('includes sourceSet for all the possible resolutions we can display the image at, given the requested width and the original source width', () => {
58
70
  expect(sourceSet.length).toEqual(5)
59
71
  expect(sourceSet[0]?.dpr).toEqual(1)
60
72
  expect(sourceSet[0]?.url).toMatch(/dpr=1/)
@@ -65,10 +77,8 @@ describe('Image', () => {
65
77
 
66
78
  describe("An image that that isn't wide enough to generate high-resolution sourceSet for", () => {
67
79
  it('returns an array with a single resolution iamge service UR', async () => {
68
- const model = new CAPIImage(
69
- { ...mockImage, pixelWidth: 1500, pixelHeight: 500 },
70
- context
71
- )
80
+ getImageMetadata.mockResolvedValue({ width: 1500, height: 500 })
81
+ const model = new CAPIImage(mockImage, context)
72
82
  const sourceSet = await model.sourceSet({ width: 1000 })
73
83
  expect(sourceSet.length).toEqual(1)
74
84
  expect(sourceSet[0]).toEqual({
@@ -81,10 +91,8 @@ describe('Image', () => {
81
91
 
82
92
  describe('An image that that is smaller than the width that was requested', () => {
83
93
  it('returns the image at the same width as the original', async () => {
84
- const model = new CAPIImage(
85
- { ...mockImage, pixelWidth: 500, pixelHeight: 500 },
86
- context
87
- )
94
+ getImageMetadata.mockResolvedValue({ width: 500, height: 500 })
95
+ const model = new CAPIImage(mockImage, context)
88
96
  const sourceSet = await model.sourceSet({ width: 1000 })
89
97
  expect(sourceSet.length).toEqual(1)
90
98
  expect(sourceSet[0]).toEqual({
@@ -97,10 +105,8 @@ describe('Image', () => {
97
105
 
98
106
  describe('An image that that is bigger than the maximum width that was requested', () => {
99
107
  it('returns the image at the same width as the maximum width', async () => {
100
- const model = new CAPIImage(
101
- { ...mockImage, pixelWidth: 4000, pixelHeight: 4000 },
102
- context
103
- )
108
+ getImageMetadata.mockResolvedValue({ width: 4000, height: 4000 })
109
+ const model = new CAPIImage(mockImage, context)
104
110
  const sourceSet = await model.sourceSet({ width: MAX_IMAGE_WIDTH })
105
111
  expect(sourceSet.length).toEqual(1)
106
112
  expect(sourceSet[0]?.width).toEqual(MAX_IMAGE_WIDTH)
@@ -109,10 +115,8 @@ describe('Image', () => {
109
115
 
110
116
  describe('A large image with the maxDpr argumet passed', () => {
111
117
  it('only returns sourceSet up to and including the maxDpr', async () => {
112
- const model = new CAPIImage(
113
- { ...mockImage, pixelWidth: 5000, pixelHeight: 500 },
114
- context
115
- )
118
+ getImageMetadata.mockResolvedValue({ width: 5000, height: 500 })
119
+ const model = new CAPIImage(mockImage, context)
116
120
  const sourceSet = await model.sourceSet({ width: 1000, maxDpr: 2 })
117
121
  expect(sourceSet.length).toEqual(2)
118
122
  expect(sourceSet[0]?.dpr).toEqual(1)
@@ -133,6 +137,32 @@ describe('Image', () => {
133
137
  await expect(
134
138
  model.sourceSet({ width: 1000, maxDpr: 2 })
135
139
  ).rejects.toThrow('not-a-uuid is not a valid Content API Image ID')
140
+ expect(getImageMetadata).not.toHaveBeenCalled()
141
+ })
142
+ })
143
+
144
+ describe('When the image service fails to return metadata', () => {
145
+ let sourceSet: ImageSource[]
146
+ beforeEach(async () => {
147
+ getImageMetadata.mockRejectedValue(null)
148
+ const model = new CAPIImage(mockImage, context)
149
+ sourceSet = await model.sourceSet({ width: 3000 })
150
+ })
151
+ it('returns an image service URL with the requested width and max DPR 2', () => {
152
+ expect(sourceSet).toMatchInlineSnapshot(`
153
+ Array [
154
+ Object {
155
+ "dpr": 1,
156
+ "url": "https://www.ft.com/__origami/service/image/v2/images/raw/ftcms%3A00000000-0000-0000-0000-000000000001?source=image-test&fit=scale-down&quality=highest&width=3000&dpr=1",
157
+ "width": 3000,
158
+ },
159
+ Object {
160
+ "dpr": 2,
161
+ "url": "https://www.ft.com/__origami/service/image/v2/images/raw/ftcms%3A00000000-0000-0000-0000-000000000001?source=image-test&fit=scale-down&quality=highest&width=3000&dpr=2",
162
+ "width": 3000,
163
+ },
164
+ ]
165
+ `)
136
166
  })
137
167
  })
138
168
  })
@@ -211,18 +241,17 @@ describe('Image', () => {
211
241
  })
212
242
 
213
243
  describe('width/height', () => {
214
- it('returns the original width and height from capi data', async () => {
244
+ it('returns the original width and height from the image service metadata', async () => {
245
+ getImageMetadata.mockResolvedValue({ width: 100, height: 200 })
215
246
  const model = new CAPIImage(mockImage, context)
216
247
  const { width, height } = (await model.dimensions()) || {}
217
- expect(width).toEqual(5000)
218
- expect(height).toEqual(1000)
248
+ expect(width).toEqual(100)
249
+ expect(height).toEqual(200)
219
250
  })
220
251
 
221
- it('returns null if no width and height in capi data', async () => {
222
- const model = new CAPIImage(
223
- { ...mockImage, pixelHeight: undefined, pixelWidth: undefined },
224
- context
225
- )
252
+ it('returns null if the image service metadata call fails', async () => {
253
+ getImageMetadata.mockRejectedValue(null)
254
+ const model = new CAPIImage(mockImage, context)
226
255
  const dimensions = await model.dimensions()
227
256
  expect(dimensions).toEqual(null)
228
257
  })
@@ -11,7 +11,8 @@ import {
11
11
  validLiteralUnionValue,
12
12
  } from '../resolvers/literal-union'
13
13
  import { ImageFormat, ImageType } from '../resolvers/scalars'
14
- import { BaseError } from '@dotcom-reliability-kit/errors'
14
+ import isError from '../helpers/isError'
15
+ import { BaseError, OperationalError } from '@dotcom-reliability-kit/errors'
15
16
  import type { ImageSource } from '../generated'
16
17
 
17
18
  export type ImageSourceArgs = {
@@ -156,18 +157,25 @@ export class CAPIImage implements Image {
156
157
  height: this.capiImage.pixelHeight,
157
158
  }
158
159
  }
159
-
160
- // HACK:KB:20241029 graphics and images in layouts don't have pixelWidth/
161
- // pixelHeight because of reasons. we want to avoid calling Image Service
162
- // to improve performance. skip calling Image Service for the time being
163
- this.context.logger.warn({
164
- event: 'SKIPPING_IMAGE_DIMENSIONS',
165
- message: 'Not calling Image Service to get graphics dimensions',
166
- url: this.capiImage.binaryUrl,
167
- type: this.type(),
168
- })
169
-
170
- return null
160
+ try {
161
+ const imageMetadata = await this.#dataSources.origami.getImageMetadata(
162
+ this.capiImage.binaryUrl
163
+ )
164
+ return imageMetadata
165
+ } catch (error) {
166
+ if (isError(error)) {
167
+ this.context.logger.warn({
168
+ event: 'RECOVERABLE_ERROR',
169
+ error: new OperationalError({
170
+ code: 'IMAGE_DIMENSIONS_ERROR',
171
+ message: `Failed to get dimensions for ${this.capiImage.binaryUrl}`,
172
+ url: this.capiImage.binaryUrl,
173
+ cause: error,
174
+ }),
175
+ })
176
+ }
177
+ return null
178
+ }
171
179
  }
172
180
 
173
181
  credit() {
@@ -95,6 +95,25 @@ describe('produces the correct types', () => {
95
95
  expect(desiredColour).toEqual(actualColour)
96
96
  })
97
97
 
98
+ it('full bleed left topper', () => {
99
+ const splitTopper = {
100
+ headline: '',
101
+ standfirst: '',
102
+ backgroundColour: 'auto',
103
+ layout: 'full-bleed-left',
104
+ }
105
+
106
+ const clonedBase = cloneDeep(baseCapiObject)
107
+ clonedBase.topper = splitTopper
108
+ const capiResponse = new CapiResponse(clonedBase, context)
109
+ const topper = new Topper(capiResponse, context)
110
+
111
+ const desired = 'FullBleedTopper'
112
+ const actual = topper.type()
113
+
114
+ expect(actual).toEqual(desired)
115
+ })
116
+
98
117
  it('opinion topper, is normal headline, sky background', () => {
99
118
  const clonedBase = cloneDeep(baseCapiObject)
100
119
  clonedBase.annotations.push({
@@ -79,10 +79,6 @@ export class Topper {
79
79
  return 'SplitTextTopper'
80
80
  }
81
81
 
82
- if (this.capiResponse.topper()?.layout === 'full-bleed-image-left') {
83
- return 'SplitTextTopper'
84
- }
85
-
86
82
  if (this.capiResponse.topper()?.layout === 'flourish') {
87
83
  return 'TopperWithFlourish'
88
84
  }