├── .env.example ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── lib └── find-existing-comment.js ├── package.json ├── script └── cibuild └── test ├── __snapshots__ └── index.test.js.snap ├── fixtures ├── afterFile.json ├── afterImages.json ├── beforeFile.json ├── beforeImages.json └── diff.txt ├── index.test.js └── lib └── find-existing-comment.test.js /.env.example: -------------------------------------------------------------------------------- 1 | # The ID of your GitHub App 2 | APP_ID= 3 | WEBHOOK_SECRET=development 4 | 5 | # Use `trace` to get verbose logging or `info` to show less 6 | LOG_LEVEL=debug 7 | 8 | # Go to https://smee.io/new set this to the URL that you are redirected to. 9 | # WEBHOOK_PROXY_URL= 10 | 11 | FIGMA_TOKEN=1234 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | *.pem 4 | .env 5 | package-lock.json 6 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | [fork]: /fork 4 | [pr]: /compare 5 | [style]: https://standardjs.com/ 6 | [code-of-conduct]: CODE_OF_CONDUCT.md 7 | 8 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 9 | 10 | Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms. 11 | 12 | ## Submitting a pull request 13 | 14 | 1. [Fork][fork] and clone the repository 15 | 1. Configure and install the dependencies: `npm install` 16 | 1. Make sure the tests pass on your machine: `npm test`, note: these tests also apply the linter, so no need to lint seperately 17 | 1. Create a new branch: `git checkout -b my-branch-name` 18 | 1. Make your change, add tests, and make sure the tests still pass 19 | 1. Push to your fork and [submit a pull request][pr] 20 | 1. Pat your self on the back and wait for your pull request to be reviewed and merged. 21 | 22 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 23 | 24 | - Follow the [style guide][style] which is using standard. Any linting errors should be shown when running `npm test` 25 | - Write and update tests. 26 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests. 27 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 28 | 29 | Work in Progress pull request are also welcome to get feedback early on, or if there is something blocked you. 30 | 31 | ## Resources 32 | 33 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 34 | - [Using Pull Requests](https://help.github.com/articles/about-pull-requests/) 35 | - [GitHub Help](https://help.github.com) 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 GitHub Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Figma Image Diff probot 2 | 3 | > a GitHub App built with [probot](https://github.com/probot/probot) that checks before and after components between two Figma files and posts before after images. 4 | 5 | ![image](https://user-images.githubusercontent.com/54012/38582457-a76db572-3cc4-11e8-8b95-34345c8beab8.png) 6 | 7 | ## Usage 8 | 9 | This probot is customized to work with [Octicons](https://github.com/primer/octicons) to look for changes in the figma import url found in the octicons package.json. 10 | 11 | When there is a change in the url, this bot will pull down images for the before and after files and generate a before and after image for any changed components. 12 | 13 | ## Documentation 14 | 15 | The documentation for writing your own Probot can be [found on the probot website](https://probot.github.io/). 16 | 17 | ## License 18 | 19 | [MIT](./LICENSE) © [GitHub](https://github.com/) 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const got = require('got') 2 | const findExistingComment = require('./lib/find-existing-comment') 3 | 4 | async function getFigmaComponents (figmaFileKey) { 5 | const response = await got.get(`https://api.figma.com/v1/files/${figmaFileKey}`, { 6 | headers: { 'Content-Type': 'application/json', 'x-figma-token': process.env.FIGMA_TOKEN }, 7 | json: true 8 | }) 9 | 10 | let components = {} 11 | const check = (c) => { 12 | if (c.type === 'COMPONENT') { 13 | components[c.id] = { 14 | name: c.name, 15 | id: c.id, 16 | description: response.body.components[c.id].description, 17 | raw: JSON.stringify(c) 18 | } 19 | } else if (c.children) { 20 | c.children.forEach(check) 21 | } 22 | } 23 | response.body.document.children.forEach(check) 24 | return components 25 | } 26 | 27 | async function getFigmaImages (figmaFileKey, componentIds) { 28 | const response = await got.get(`https://api.figma.com/v1/images/${figmaFileKey}`, { 29 | query: { 30 | ids: componentIds, 31 | format: 'svg' 32 | }, 33 | headers: { 'Content-Type': 'application/json', 'x-figma-token': process.env.FIGMA_TOKEN }, 34 | json: true 35 | }) 36 | const { images } = response.body 37 | return Promise.all(Object.keys(images).map(async id => { 38 | const url = images[id] 39 | const res = await got.get(url, { headers: { 'Content-Type': 'images/svg+xml' } }) 40 | return { url, id, raw: res.body } 41 | })) 42 | } 43 | 44 | const hasChanged = (before, after) => { 45 | if (before.name !== after.name || 46 | before.description !== after.description || 47 | before.raw !== after.raw || 48 | before.image.raw !== after.image.raw) { 49 | return true 50 | } 51 | return false 52 | } 53 | 54 | // Template 55 | const changeComment = (data) => { 56 | return ` 57 | | Before | After | 58 | | :-- | :-- | 59 | ${Object.values(data.before.components).map((b) => { 60 | const a = data.after.components[b.id] 61 | return `| **Name:** \`${b.name}\`
**Description:** \`${b.description}\` [](${b.image.url}) | **Name:** \`${a.name}\`
**Description:** \`${a.description}\` [](${a.image.url}) |` 62 | }).join('\n')}` 63 | } 64 | 65 | async function createOrUpdateComment (context, params) { 66 | const existingComment = await findExistingComment(context) 67 | if (existingComment) { 68 | return context.github.issues.editComment(context.issue({ 69 | comment_id: existingComment.id, 70 | body: params.body 71 | })) 72 | } else { 73 | return context.github.issues.createComment(params) 74 | } 75 | } 76 | 77 | module.exports = robot => { 78 | // Will trigger whenever a new PR is opened or pushed to 79 | robot.on(['pull_request.opened', 'pull_request.synchronize'], async context => { 80 | // Get the diff of the PR 81 | const diff = (await context.github.pullRequests.get(context.issue({ 82 | headers: { Accept: 'application/vnd.github.diff' } 83 | }))).data 84 | 85 | let data = {} 86 | 87 | diff.match(/^[-+]\s.*www\.figma\.com\/file\/.+\//gm).forEach(m => { 88 | data[m[0] === '-' ? 'before' : 'after'] = { 89 | 'fileId': /www\.figma\.com\/file\/(.+)\//.exec(m).pop() 90 | } 91 | }) 92 | 93 | if (data.before.fileId && data.after.fileId) { 94 | // Get Before components 95 | data.before.components = (await getFigmaComponents(data.before.fileId)) 96 | 97 | // Get After components 98 | data.after.components = (await getFigmaComponents(data.after.fileId)) 99 | 100 | // Get Before images 101 | let bimages = (await getFigmaImages(data.before.fileId, Object.keys(data.before.components).join(','))) 102 | 103 | bimages.forEach(bi => { 104 | data.before.components[bi.id].image = bi 105 | }) 106 | 107 | // Get After images 108 | let aimages = (await getFigmaImages(data.after.fileId, Object.keys(data.after.components).join(','))) 109 | 110 | aimages.forEach(ai => { 111 | data.after.components[ai.id].image = ai 112 | }) 113 | 114 | // Mark any that changed on the surface (no data in first call) 115 | Object.keys(data.before.components).forEach((k) => { 116 | if (!hasChanged(data.before.components[k], data.after.components[k])) { 117 | delete data.before.components[k] 118 | delete data.after.components[k] 119 | } 120 | }) 121 | 122 | // Exit early if no components have changed 123 | if (Object.keys(data.before.components).length === 0 || 124 | Object.keys(data.after.components).length === 0) return 125 | 126 | const params = context.issue({body: changeComment(data)}) 127 | 128 | return createOrUpdateComment(context, params) 129 | } 130 | }) 131 | } 132 | -------------------------------------------------------------------------------- /lib/find-existing-comment.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns the existing comment created by this app if it exists 3 | */ 4 | module.exports = async function findExistingComment (context) { 5 | const comments = await context.github.issues.listComments(context.issue({ per_page: 100 })) 6 | return comments.data.find(comment => { 7 | return comment.user.type === 'Bot' && comment.body.startsWith('') 8 | }) 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@github/figma-diff-probot", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "license": "MIT", 7 | "repository": "https://github.com/primer/figma-diff-probot.git", 8 | "scripts": { 9 | "start": "probot run ./index.js", 10 | "test": "jest && standard" 11 | }, 12 | "dependencies": { 13 | "got": "^8.3.0", 14 | "probot": "^6.0.0" 15 | }, 16 | "devDependencies": { 17 | "jest": "^21.2.1", 18 | "nock": "^9.2.5", 19 | "smee-client": "^1.0.1", 20 | "standard": "^10.0.3" 21 | }, 22 | "engines": { 23 | "node": ">= 8.3.0" 24 | }, 25 | "standard": { 26 | "env": [ 27 | "jest" 28 | ] 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /script/cibuild: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Running tests" 4 | -------------------------------------------------------------------------------- /test/__snapshots__/index.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`figma-diff-probot creates a comment with a before and after image 1`] = ` 4 | Array [ 5 | Array [ 6 | Object { 7 | "body": " 8 | | Before | After | 9 | | :-- | :-- | 10 | | **Name:** \`heart\`
**Description:** \`keywords: love, beat\` [](https://images.com/before) | **Name:** \`heart\`
**Description:** \`keywords: love, beat\` [](https://images.com/after) |", 11 | "number": 1, 12 | "owner": "primer", 13 | "repo": "octicons", 14 | }, 15 | ], 16 | ] 17 | `; 18 | 19 | exports[`figma-diff-probot updates the existing comment 1`] = ` 20 | Array [ 21 | Array [ 22 | Object { 23 | "body": " 24 | | Before | After | 25 | | :-- | :-- | 26 | | **Name:** \`heart\`
**Description:** \`keywords: love, beat\` [](https://images.com/before) | **Name:** \`heart\`
**Description:** \`keywords: love, beat\` [](https://images.com/after) |", 27 | "comment_id": undefined, 28 | "number": 1, 29 | "owner": "primer", 30 | "repo": "octicons", 31 | }, 32 | ], 33 | ] 34 | `; 35 | -------------------------------------------------------------------------------- /test/fixtures/afterFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Octicons - Fix heart icon", 3 | "lastModified": "2018-03-24T01:16:21.444Z", 4 | "thumbnailUrl": "https://s3-alpha.figma.com/img/3f85/7974/22a3f4160999cb8aae345e0026a1213a", 5 | "document": { 6 | "id": "0:0", 7 | "name": "Document", 8 | "type": "DOCUMENT", 9 | "children": [ 10 | { 11 | "id": "0:1", 12 | "name": "Octicons", 13 | "type": "CANVAS", 14 | "children": [ 15 | { 16 | "id": "0:400", 17 | "name": "heart", 18 | "type": "COMPONENT", 19 | "blendMode": "PASS_THROUGH", 20 | "children": [ 21 | { 22 | "id": "0:399", 23 | "name": "Shape", 24 | "type": "VECTOR", 25 | "blendMode": "PASS_THROUGH", 26 | "absoluteBoundingBox": { 27 | "x": 370, 28 | "y": 78, 29 | "width": 12, 30 | "height": 11 31 | }, 32 | "constraints": { 33 | "vertical": "SCALE", 34 | "horizontal": "SCALE" 35 | }, 36 | "fills": [ 37 | { 38 | "type": "SOLID", 39 | "blendMode": "NORMAL", 40 | "color": { 41 | "r": 0, 42 | "g": 0, 43 | "b": 0, 44 | "a": 1 45 | } 46 | } 47 | ], 48 | "strokes": [], 49 | "strokeWeight": 0, 50 | "strokeAlign": "CENTER", 51 | "exportSettings": [], 52 | "effects": [] 53 | } 54 | ], 55 | "absoluteBoundingBox": { 56 | "x": 370, 57 | "y": 76, 58 | "width": 12, 59 | "height": 16 60 | }, 61 | "constraints": { 62 | "vertical": "SCALE", 63 | "horizontal": "SCALE" 64 | }, 65 | "backgroundColor": { 66 | "r": 0, 67 | "g": 0, 68 | "b": 0, 69 | "a": 0 70 | }, 71 | "clipsContent": false, 72 | "layoutGrids": [], 73 | "exportSettings": [ 74 | { 75 | "suffix": "", 76 | "format": "SVG", 77 | "constraint": { 78 | "type": "SCALE", 79 | "value": 1 80 | } 81 | } 82 | ], 83 | "effects": [] 84 | } 85 | ], 86 | "backgroundColor": { 87 | "r": 1, 88 | "g": 1, 89 | "b": 1, 90 | "a": 1 91 | }, 92 | "exportSettings": [] 93 | } 94 | ] 95 | }, 96 | "components": { 97 | "0:400": { 98 | "name": "heart", 99 | "description": "keywords: love, beat" 100 | } 101 | }, 102 | "schemaVersion": 0 103 | } -------------------------------------------------------------------------------- /test/fixtures/afterImages.json: -------------------------------------------------------------------------------- 1 | { 2 | "err": null, 3 | "images": { 4 | "0:400": "https://images.com/after" 5 | } 6 | } -------------------------------------------------------------------------------- /test/fixtures/beforeFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Octicons", 3 | "lastModified": "2018-03-21T21:22:35.704Z", 4 | "thumbnailUrl": "https://s3-alpha.figma.com/img/3f85/7974/22a3f4160999cb8aae345e0026a1213a", 5 | "document": { 6 | "id": "0:0", 7 | "name": "Document", 8 | "type": "DOCUMENT", 9 | "children": [ 10 | { 11 | "id": "0:1", 12 | "name": "Octicons", 13 | "type": "CANVAS", 14 | "children": [ 15 | { 16 | "id": "0:400", 17 | "name": "heart", 18 | "type": "COMPONENT", 19 | "blendMode": "PASS_THROUGH", 20 | "children": [ 21 | { 22 | "id": "0:399", 23 | "name": "Shape", 24 | "type": "VECTOR", 25 | "blendMode": "PASS_THROUGH", 26 | "absoluteBoundingBox": { 27 | "x": 370, 28 | "y": 78, 29 | "width": 12, 30 | "height": 11 31 | }, 32 | "constraints": { 33 | "vertical": "SCALE", 34 | "horizontal": "SCALE" 35 | }, 36 | "fills": [ 37 | { 38 | "type": "SOLID", 39 | "blendMode": "NORMAL", 40 | "color": { 41 | "r": 0, 42 | "g": 0, 43 | "b": 0, 44 | "a": 1 45 | } 46 | } 47 | ], 48 | "strokes": [], 49 | "strokeWeight": 0, 50 | "strokeAlign": "CENTER", 51 | "exportSettings": [], 52 | "effects": [] 53 | } 54 | ], 55 | "absoluteBoundingBox": { 56 | "x": 370, 57 | "y": 76, 58 | "width": 12, 59 | "height": 16 60 | }, 61 | "constraints": { 62 | "vertical": "SCALE", 63 | "horizontal": "SCALE" 64 | }, 65 | "backgroundColor": { 66 | "r": 0, 67 | "g": 0, 68 | "b": 0, 69 | "a": 0 70 | }, 71 | "clipsContent": false, 72 | "layoutGrids": [], 73 | "exportSettings": [ 74 | { 75 | "suffix": "", 76 | "format": "SVG", 77 | "constraint": { 78 | "type": "SCALE", 79 | "value": 1 80 | } 81 | } 82 | ], 83 | "effects": [] 84 | } 85 | ], 86 | "backgroundColor": { 87 | "r": 1, 88 | "g": 1, 89 | "b": 1, 90 | "a": 1 91 | }, 92 | "exportSettings": [] 93 | } 94 | ] 95 | }, 96 | "components": { 97 | "0:400": { 98 | "name": "heart", 99 | "description": "keywords: love, beat" 100 | } 101 | }, 102 | "schemaVersion": 0 103 | } -------------------------------------------------------------------------------- /test/fixtures/beforeImages.json: -------------------------------------------------------------------------------- 1 | { 2 | "err": null, 3 | "images": { 4 | "0:400": "https://images.com/before" 5 | } 6 | } -------------------------------------------------------------------------------- /test/fixtures/diff.txt: -------------------------------------------------------------------------------- 1 | diff --git a/package.json b/package.json 2 | index 5459922..8a9929a 100644 3 | --- a/package.json 4 | +++ b/package.json 5 | @@ -7,7 +7,7 @@ 6 | "release": "script/clean && script/export && $(npm bin)/lerna publish --exact --since \"v$(npm info octicons version)\"" 7 | }, 8 | "figma": { 9 | - "url": "https://www.figma.com/file/BEFORE_KEY/Octicons" 10 | + "url": "https://www.figma.com/file/AFTER_KEY/Octicons-(Jon's-Changes)" 11 | }, 12 | "devDependencies": { 13 | "ava": "^0.22.0", 14 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const {createRobot} = require('probot') 2 | const fs = require('fs') 3 | const path = require('path') 4 | const nock = require('nock') 5 | const app = require('../') 6 | 7 | // Fixtures 8 | const diff = fs.readFileSync(path.join(__dirname, 'fixtures', 'diff.txt'), 'utf8') 9 | 10 | const beforeFile = require('./fixtures/beforeFile.json') 11 | const afterFile = require('./fixtures/afterFile.json') 12 | 13 | const beforeImages = require('./fixtures/beforeImages.json') 14 | const afterImages = require('./fixtures/afterImages.json') 15 | 16 | // Keys (mock Figma document keys) 17 | const BEFORE_KEY = 'BEFORE_KEY' 18 | const AFTER_KEY = 'AFTER_KEY' 19 | 20 | describe('figma-diff-probot', () => { 21 | let robot, github, event 22 | 23 | beforeEach(() => { 24 | robot = createRobot() 25 | github = { 26 | pullRequests: { 27 | get: jest.fn(() => Promise.resolve({ data: diff })) 28 | }, 29 | issues: { 30 | createComment: jest.fn(), 31 | listComments: jest.fn(() => Promise.resolve({ data: [] })), 32 | editComment: jest.fn() 33 | } 34 | } 35 | 36 | robot.auth = () => Promise.resolve(github) 37 | 38 | event = { 39 | event: 'pull_request', 40 | payload: { 41 | action: 'opened', 42 | repository: { 43 | owner: { name: 'primer' }, 44 | name: 'octicons' 45 | }, 46 | issue: { 47 | number: 1 48 | }, 49 | installation: { id: 1 } 50 | } 51 | } 52 | 53 | // Mock the Figma API 54 | nock('https://api.figma.com/v1') 55 | .get(`/files/${BEFORE_KEY}`).reply(200, beforeFile) 56 | .get(`/files/${AFTER_KEY}`).reply(200, afterFile) 57 | .get(`/images/${BEFORE_KEY}`).query({ ids: '0:400', format: 'svg' }).reply(200, beforeImages) 58 | .get(`/images/${AFTER_KEY}`).query({ ids: '0:400', format: 'svg' }).reply(200, afterImages) 59 | 60 | // Mock the endpoint that Figma uses for images 61 | nock('https://images.com') 62 | .get('/before').reply(200, 'BEFORE') 63 | .get('/after').reply(200, 'AFTER') 64 | 65 | app(robot) 66 | }) 67 | 68 | it('creates a comment with a before and after image', async () => { 69 | await robot.receive(event) 70 | 71 | expect(github.issues.createComment).toHaveBeenCalled() 72 | expect(github.issues.createComment.mock.calls).toMatchSnapshot() 73 | }) 74 | 75 | it('updates the existing comment', async () => { 76 | github.issues.listComments.mockReturnValueOnce(Promise.resolve({ data: [ 77 | { user: { type: 'Bot' }, body: ' Hi!' } 78 | ] })) 79 | await robot.receive(event) 80 | 81 | expect(github.issues.createComment).not.toHaveBeenCalled() 82 | expect(github.issues.editComment).toHaveBeenCalled() 83 | expect(github.issues.editComment.mock.calls).toMatchSnapshot() 84 | }) 85 | 86 | it('does not create a comment if there are no differences', async () => { 87 | nock.cleanAll() 88 | nock('https://api.figma.com/v1') 89 | .get(`/files/${BEFORE_KEY}`).reply(200, beforeFile) 90 | .get(`/files/${AFTER_KEY}`).reply(200, beforeFile) 91 | .get(`/images/${BEFORE_KEY}`).query({ ids: '0:400', format: 'svg' }).reply(200, beforeImages) 92 | .get(`/images/${AFTER_KEY}`).query({ ids: '0:400', format: 'svg' }).reply(200, afterImages) 93 | 94 | nock('https://images.com') 95 | .get('/before').reply(200, 'BEFORE') 96 | .get('/after').reply(200, 'BEFORE') 97 | 98 | await robot.receive(event) 99 | 100 | expect(github.issues.createComment).not.toHaveBeenCalled() 101 | }) 102 | }) 103 | -------------------------------------------------------------------------------- /test/lib/find-existing-comment.test.js: -------------------------------------------------------------------------------- 1 | const findExistingComment = require('../../lib/find-existing-comment') 2 | 3 | describe('findExistingComment', () => { 4 | let context 5 | 6 | beforeEach(() => { 7 | context = { 8 | issue: o => ({ owner: 'primer', repo: 'basecoat', number: 1, ...o }), 9 | github: { issues: { listComments: jest.fn() } } 10 | } 11 | }) 12 | 13 | it('returns the comment', async () => { 14 | const comments = [ 15 | { user: { type: 'User' }, body: 'Test' }, 16 | { user: { type: 'Bot' }, body: ' Magic!' } 17 | ] 18 | context.github.issues.listComments.mockReturnValueOnce(Promise.resolve({ data: comments })) 19 | const actual = await findExistingComment(context) 20 | expect(actual).toEqual(comments[1]) 21 | }) 22 | 23 | it('returns `undefined` if the comment does not exist', async () => { 24 | context.github.issues.listComments.mockReturnValueOnce(Promise.resolve({ data: [] })) 25 | const actual = await findExistingComment(context) 26 | expect(actual).toBe(undefined) 27 | }) 28 | }) 29 | --------------------------------------------------------------------------------